paranoid service worker
Vsevolod Rodionov, tech cutup
@jabher
let's create new in-browser app
OK. an npm lib?
no, no, no. with per-user storage
OK. CORS rest api
with cookies?
no, no, no. It will be private keys. No server-level storage
OK. browser plugin?
no, no, no. It should be for mobile browsers too
OK. localstorage & iframe postmessage?
Btw, similar API got hacked yesterday by 3rd party plugin
Well, F....k
The Problem
data application on client
trusted platform
Trusted platform
interface Platform {
sensitive: never;
getKeys (): Key[];
sign (Data, Key): Signature;
}
interface API extends Platform {
verify (Data, Signature): Key;
}
Trusted platform
enum Features {
OFFLINE,
CONFIRMATIONS,
MOBILE_SUPPORT,
MITM_PROTECTION,
SERVER_HIJACK_DETECT,
BAD_PLUGIN_PROTECTION
}
Trusted platform
interface Platform {
requirements: Possible,
browsers:
| Chrome | Firefox
| Edge | Safari,
platform:
| Windows | MacOS | Linux
| iOS<{jailbroken: false}>
| Android<{rooted: false}>
}
baseline
<iframe
src="https://our.app/sign?0xDEADBEEF">
<!doctype html>
<script src="/handle.js"
integrity="sha384-..."/>
<script>
window.parent.postMessage(
await handle(location)
)
</script>
</iframe>
Attack vectors
- MITM: Control transport level (OSI)
- Server: Control data layer (OSI)
- Browser host frame context:
- read/write postMessage channel
- Browser our frame context:
- read/write localStorage
- read/write/block postMessage channel
- Browser plugins:
- read/write/cancel/reject network requests
- inject in every HTTP(s) page. See "frame context"
Attacks
- 😱 Private keys stolen
- 😩 API not working
- 😒 Device flooded
- 🤔 API call data stolen
the solution
Disclaimer:
- Security audit in progress. Do not try that
at homein production. Contact me (@jabher everywhere) if you really want to try it. - Examples are written in pseudo-code
baseline
<iframe
src="https://our.app/sign?0xDEADBEEF">
<!doctype html>
<script src="/handle.js"
integrity="sha384-..."/>
<script>
window.parent.postMessage(
await handle(location, localStorage)
)</script></iframe>
MITM_PROTECTION
- DNS intrusion => DNSSEC
- HTTP intrusion => HTTPS
- HTTP -> HTTPS switch
=> HTTP header: Strict-Transport-Security - Initial delivery
=> HSTS preload list - Non-authorized certificate
=> Certificate transparency - Weak SSL/TLS
=> TLS 1.3
TLS/SSL vulnerabilities
TLS/SSL vulnerabilities
- Logjam
- NOMORE
- Bar Mitzvah
- SWEET32
- POODLE
- Heartbleed
- DROWN
- CRIME
- BEAST
- BREACH
- FREAK
untrusted frame
const frame = createIframe(frameUrl)
const mo = new MutationObserver(
mutations => throw new Error()
)
mo.observe(iframe, {
attributes: true
})
document.body.appendChild(frame)
untrusted frame
const {publicKey, privateKey} = createKeys()
const blobFrameUrl = URL.createObjectURL(new Blob([`
<iframe src=${iframeUrl}/>
<script>
window.onmessage = ({data}) =>
window.parent.postMessage(
encrypt(data, ${publicKey})
)
</script>
`], {
type: 'text/html'
}))
untrusted
frame
untrusted server
const {ok, headers} = await fetch(frameUrl, {
integrity: 'sha384-...',
redirect: 'manual',
cache: 'force-cache',
mode: 'cors'
})
assert(
ok,
headers.get('Cache-Control') === 'max-age=84600',
headers.has('pragma') === false
)
injectIframe(frameUrl)
untrusted server
untrusted cache
//iframe.html
await navigator.serviceWorker.register('sw.js')
//sw.js
self.addEventListener('fetch', event =>
event.respondWith(new Response(`
<!doctype html>
<script>
window.parent.postMessage(
await handle(location)
)
</script>
`)))))
service worker
service worker
untrusted cache
//iframe.html
await navigator.serviceWorker.register('sw.js')
//sw.js
self.addEventListener('fetch', event =>
event.respondWith(new Response(`
<!doctype html>
<script>
window.parent.postMessage(
await handle(location)
)
</script>
`)))))
Service worker
& origin
only allow-same-origin should be required for SW interception within a sandboxed iframe
By the time the request gets to fetch, it doesn't know it came from a sandboxed iframe
untrusted args
doSomethingImportant = async (...args) => {
...
await notification('confirm?', {
requireInteraction: true,
data: args,
actions: [
{action: 'res', title: 'yes'},
{action: 'rej', title: 'no'}
]
})
...
}
untrusted args
// sw.js
self.addEventListener(
'notificationclick',
({notification, action}) => {
notification.close()
actions.callback(
notification.data,
action
)
})
untrusted args
self.addEventListener(
'fetch',
async event =>
event.respondWith(new Response(`
<script>
window.parent.postMessage(
${
JSON.stringify(
await handle(event.request.url)
)
})`)))
untrusted plugins
untrusted
plugins
origin: null
- iframe sandbox=not allow-same-origin
- data: URI
- blob: URI (в SW не работает)
- file:// URI
- chrome-extension://
origin: null
event.respondWith(Response.redirect(
'data:text/html,' +
encodeURIComponent(`
<script>parent.postMessage(${
JSON.stringify(
await handle(request.url)
)
})`))))
uninterceptable
const response = await sendTo(
`wss://${query.remote}`,
await handle(query.args)
)
return 'data:text/html,' +
encodeURIComponent(`
<script>parent.postMessage(${
JSON.stringify(response)
})`)
the result
what's secured
- Client lib verifies SW scripts from server
- User verifies per-website actions
- Offline/server fault - friendly
- SW -> server channel is safe
what's not mentioned
- private keys import & export
- app management
- platform updates & lib releases
- lock on security breach
- failsafe (auto-delete) storage
Vsevolod Rodionov, tech cutup
@jabher
paranoid service worker
Paranoid Service Worker Secr
By jabher
Paranoid Service Worker Secr
- 1,096