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:

  1. Security audit in progress. Do not try that at home in production. Contact me (@jabher everywhere) if you really want to try it.
  2. 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

  1. DNS intrusion => DNSSEC
  2. HTTP intrusion => HTTPS
  3. HTTP -> HTTPS switch
    => HTTP header: Strict-Transport-Security
  4. Initial delivery
    => HSTS preload list
  5. Non-authorized certificate
    => Certificate transparency
  6. 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

w3c/ServiceWorker/issues/648

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