joev.dev - thoughts, engineering, art, life
Posts About
written Nov 09 2022

I'm joev, a security engineer 👋 You may know me from my work at Apple or on Metasploit, or from my CVEs.

Nowadays I work on open-source security software. This site is a cryptographic experiment of sorts, and a place to store my photos.

Say hello: @joev@infosec.exchange

Forced cooperation: Using a browser origin without trusting it

Occasionally I have run into the problem in browser security where a variety of unrelated domains need to trust a single origin with some responsibility. What origin should be used, and who should run that service? This comes up when building certain kinds of SSO services, so I'll get into it here for future reference.

An example

Imagine, for example, that you are implementing a shared localStorage-backed database of errors logged by your company's web apps. You choose to do this on the origin domain https://errorlog.corp.com. You want your other domains to be able to send errors to the local database, so you implement an <iframe> postMessage bridge on errorlog.corp.com that allows the other domains to access a postMessage API for storing logs locally under the errorlog.corp.com origin's localStorage.

Unfortunately this also means an attacker that compromises errorlog.corp.com and simply waits will obtain eventual access to all the users' local error logs. What would be useful for this scenario, is if a site could apply an integrity check (à la "subresource integrity") to any HTML loaded in a cross-origin iframe or subsequent navigation. Unfortunately this idea was dropped from iframes in the spec early on, so we must find another route.

(Ab)use the cache, Luke

One way to achieve this is by leveraging the browser cache. This behavior is not explicitly guaranteed by any browser spec (user agents sometimes have caches disabled), but modern browsers will all act appropriately enough and let you do this.

First, we'll write the HTML document meant for the errorlog.corp.com iframe. Make sure to serve the response with all the caching headers, and CORS allowed:

HTTP/1.1 200 OK
...
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 1991 22:22:22 GMT
ETag: "33a64df5"
Access-Control-Allow-Origin: https://otherdomain.com

Finally, our integrating page will hesitantly fetch() the document, inspect its hash, and double-check that the response contained appropriate cache headers. If it all checks out, load the iframe or navigation, comfortable that the expected value is cached:

const opts = { 
  mode: 'cors', cache: 'force-cache', redirect: 'error', 
  // this is checked by fetch() against the response body
  integrity: 'sha256-e1DGeNcok+fcRw3M6TPLt97Yn+1etfTt2DTFO5dudso='
};

fetch('https://errorlog.corp.com/?v=1', opts).then((res) => {
  // check that cache headers are as expected
  const cacheCtrl = res.headers.get('cache-control');
  const lastModified = res.headers.get('last-modified');
  if (cacheCtrl !== 'public, max-age=31536000, immutable' ||
      lastModified !== 'Tue, 22 Feb 1991 22:22:22 GMT') {
    // Error; result is not cached. Fail.
    alert("Failed to load logStorage integration due to bad cache headers.");
    return;
  }

  // everything looks good, load the frame
  loadIframe('https://errorlog.corp.com/?v=1');
});

Downsides to this approach

While this check mitigates the scenario where an attacker has compromised the shared origin and poisoned the relevant HTML, some constrained attacks are still possible. For example, an attacker that can manage to navigate a logged-in user to a different, uncached, poisoned route on the compromised server could then execute Javascript with the privileges of the origin. However, this is a more constrained and targeted attack; put another way, the above mitigation prevents many, but not all attacks that can lead to malicious content loaded in the sensitive origin.

The other downside is versioning. What if the third-party site wants to push an update? A URL versioning scheme can be used (?v=1.2), and the client would opt-in to using and verifying the new version of the site. This limits the velocity of such updates, but I don't think this is the end of the world in cases where you would want to use such security guarantees.

When should I use this?

Very rarely, probably never.

I do wish subresource integrity were explicitly extended to things like iframes and navigations, to alleviate these sort of situations, but for now I'll just be happy that fetch() has integrity support.