Contributors: Artur Janc, Camille Lamy, Charlie Reis, Jun Kokatsu, Mike West. Patches and corrections welcome!
Last Updated: Mar. 12th, 2021
In early 2018, Spectre made it clear that a foundational security boundary the web aimed to maintain was substantially less robust than expected. This revelation has pushed web browsers to shift their focus from the platform-level origin boundary to an OS-level process boundary. Chromium‘s threat model, for instance, now asserts that “active web content … will be able to read any and all data in the address space of the process that hosts it”. This shift in thinking imposes a shift in development practice, both for browser vendors, and for web developers. Browsers need to align the origin boundary with the process boundary through fundamental refactoring projects (for example, Chromium’s Site Isolation, and Firefox's Project Fission). Moreover, browsers must provide web developers with tools to mitigate risk in the short term, and should push the platform towards safe default behaviors in the long term. The bad news is that this is going to be a lot of work, much of it falling on the shoulders of web developers. The good news is that a reasonable set of mitigation primitives exists today, ready and waiting for use.
This document will point to a set of mitigations which seem promising, and provide concrete recommendations for web developers responsible for protecting users' data.
Spectre-like side-channel attacks inexorably lead to a model in which active web content (JavaScript, WASM, probably CSS if we tried hard enough, and so on) can read any and all data which has entered the address space of the process which hosts it. While this has deep implications for user agent implementations' internal hardening strategies (stack canaries, ASLR, etc), here we'll remain focused on the core implication at the web platform level, which is both simple and profound: any data which flows into a process hosting a given origin is legible to that origin. We must design accordingly.
In order to determine the scope of data that can be assumed accessible to an attacker, we must make a few assumptions about the normally-not-web-exposed process model which the user agent implements. The following seems like a good place to start:
User agents are capable of separating the execution of a web origin‘s code into a process distinct from the agent’s core. This separation enables the agent itself to access local devices, fetch resources, broker cross-process communication, and so on, in a way which remains invisible to any process potentially hosting untrusted code.
User agents are able to make decisions about whether or not a given resource should be delivered to a process hosting a given origin based on characteristics of both the request and the response (headers, etc).
User agents can consistently separate top-level, cross-origin windows into distinct processes. They cannot consistently separate same-site or same-origin windows into distinct processes given the potential for synchronous access between the windows.
User agents cannot yet consistently separate framed origins into processes distinct from their embedders' origin.
Note: Though some user agents support out-of-process frames, no agent supports it consistently across a broad range of devices and platforms. Ideally this will change over time, as the frame boundary must be one we can eventually consider robust.
With this in mind, our general assumption will be that an origin gains access to any resource which it renders (including images, stylesheets, scripts, frames, etc). Likewise, embedded frames gain access to their ancestors' content.
This model is spelled out in more detail in both Chromium‘s Post-Spectre Threat Model Rethink, and in Artur Janc’s Notes on the threat model of cross-origin isolation.
Decide when (not!) to respond to requests by examining incoming headers, paying special attention to the Origin
header on the one hand, and various Sec-Fetch-
prefixed headers on the other, as described in the article Protect your resources from web attacks with Fetch Metadata.
Restrict attackers' ability to load your data as a subresource by setting a cross-origin resource policy (CORP) of same-origin
(opening up to same-site
or cross-origin
only when necessary).
Restrict attackers' ability to frame your data as a document by opt-ing into framing protections via X-Frame-Options: SAMEORIGIN
or CSP's more granular frame-ancestors
directive (frame-ancestors 'self' https://trusted.embedder
, for example).
Restrict attackers' ability to obtain a handle to your window by setting a cross-origin opener policy (COOP). In the best case, you can default to a restrictive same-origin
value, opening up to same-origin-allow-popups
or unsafe-none
only if necessary.
Prevent MIME-type confusion attacks and increase the robustness of passive defenses like cross-origin read blocking (CORB) / opaque response blocking (ORB) by setting correct Content-Type
headers, and globally asserting X-Content-Type-Options: nosniff
.
Resources which are intended to be loaded into documents should protect themselves from being used in unexpected ways. Before walking through strategies for specific kinds of resources, a few headers seem generally applicable:
Sites should use Fetch Metadata to make good decisions about when to serve resources. In order to ensure that decision sticks, servers should explain its decision to the browser by sending a Vary
header containing Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site
. This ensures that the server has a chance to make different decisions for requests which will be used differently.
Subresources should opt-out of MIME type sniffing by sending an X-Content-Type-Options
header with a value of nosniff
. This increases the robustness of MIME-based checks like cross-origin read blocking and opaque response blocking, and mitigates some well-known risks around type confusion for scripts.
Subresources are intended for inclusion in a given context, not as independently navigable documents. To mitigate the risk that navigation to a subresource causes script execution or opens an origin up to attack in some other way, servers can assert the following set of headers which collectively make it difficult to meaningfully abuse a subresource via navigation:
Use the Content-Security-Policy
header to assert the sandbox
directive. This ensures that these resources remain inactive if navigated to directly as a top-level document. No scripts will execute, and the resource will be pushed into an “opaque” origin.
Note: Some servers deliver Content-Disposition: attachment; filename=file.name
to obtain a similar effect. This was valuable to mitigate vulnerabilies in Flash, but the sandbox
approach seems to more straightforwardly address the threats we care about today.
Asserting the Cross-Origin-Opener-Policy
header with a value of same-origin
prevents cross-origin documents from retaining a handle to the resource‘s window if it’s opened in a popup.
Sending the X-Frame-Options
header with a value of DENY
prevents the resource from being framed.
Most subresources, then, should contain the following block of headers, which you'll see repeated a few times below:
Content-Security-Policy: sandbox Cross-Origin-Opener-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: DENY
With these generic protections in mind, let's sift through a few scenarios to determine what headers a server would be well-served to assert:
By their nature, static resources contain the same data no matter who requests them, and therefore cannot contain interesting information that an attacker couldn‘t otherwise obtain. There’s no risk to making these resources widely available, and value in allowing embedders to robustly debug, so something like the following response headers could be appropriate:
Access-Control-Allow-Origin: * Cross-Origin-Resource-Policy: cross-origin Timing-Allow-Origin: * Content-Security-Policy: sandbox Cross-Origin-Opener-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: DENY
CDNs are the canonical static resource distribution points, and many use the pattern above. Take a look at the following common resources' response headers for inspiration:
https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js
https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
https://ssl.google-analytics.com/ga.js
Similarly, application-specific static resource servers are a good place to look for this practice. Consider:
https://static.xx.fbcdn.net/rsrc.php/v3/y2/r/zVvRrO8pOtu.png
https://www.gstatic.com/images/branding/googlelogo/svg/googlelogo_clr_74x24px.svg
Subresources that contain data personalized to a given user are juicy targets for attackers, and must be defended by ensuring that they're loaded only in ways that are appropriate for the data in question. A few cases are well worth considering:
Application-internal resources (private API endpoints, avatar images, uploaded data, etc.) should not be available to any cross-origin requestor. These resources should be restricted to usage as a subresource in same-origin contexts by sending a Cross-Origin-Resource-Policy
header with a value of same-origin
:
Cross-Origin-Resource-Policy: same-origin Content-Security-Policy: sandbox Cross-Origin-Opener-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: DENY
This header will prevent cross-origin attackers from loading the resource as a response to a no-cors
request.
For example, examine the headers returned when requesting endpoints like the following:
https://myaccount.google.com/_/AccountSettingsUi/browserinfo
https://twitter.com/i/api/1.1/branch/init.json
https://www.facebook.com/ajax/webstorage/process_keys/?state=0
Personalized resources intended for cross-origin use (public API endpoints, etc) should carefully consider incoming requests' properties before responding. These endpoints can only safely be enabled by requiring CORS, and choosing the set of origins for which a given response can be exposed by setting the appropriate access-control headers, for example:
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: https://trusted.example Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: ... Access-Control-Allow-...: ... Cross-Origin-Resource-Policy: same-origin Content-Security-Policy: sandbox Cross-Origin-Opener-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: DENY
Note: The Cross-Origin-Resource-Policy
header is only processed for requests that are not using CORS for access control (“no-cors
requests”). Sending Cross-Origin-Resource-Policy: same-origin
is therefore not harmful, and works to ensure that no-cors
usage isn't accidentally allowed.
For example, examine the headers returned when requesting endpoints like the following:
https://api.twitter.com/1.1/jot/client_event.json
https://play.google.com/log?format=json&hasfast=true
https://securepubads.g.doubleclick.net/pcs/view
https://c.amazon-adsystem.com/e/dtb/bid
Personalized resources that are intended for cross-origin no-cors
embedding, but which don't intend to be directly legible in that context (avatar images, authenticated media, etc). These should enable cross-origin embedding via Cross-Origin-Resource-Policy
, but not via CORS access control headers:
Cross-Origin-Resource-Policy: cross-origin Content-Security-Policy: sandbox Cross-Origin-Opener-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: DENY
Note, that this allows the resource to be used by any cross-origin document. That‘s reasonable for some use cases, but requiring CORS, and opting-in a small set of origins via appropriate access-control headers is a possible alternative for some resources. This approach will give those contexts trivial access to the resource’s bits, so the granularity is a tradeoff. Still, considering this case to be the same as the “personalized resources intended for cross-origin use” isn't unreasonable.
If we implemented more granular bindings for CORP headers (along the lines of Cross-Origin-Resource-Policy: https://trusted.example
), we could avoid this tradeoff entirely. That's proposed in whatwg/fetch#760.
For example:
https://lh3.google.com/u/0/d/1JBUaX1xSOZRxBk5bRNZWgnzyJoCQC52TIRokACBSmGc=w512
Documents that require users to be signed-in almost certainly contain information that shouldn't be revealed to attackers. These pages should take care to isolate themselves from other origins, both by making a priori decisions about whether to serve the page at all, and by giving clients careful instructions about how the page can be used once delivered. For instance, something like the following set of response headers could be appropriate:
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Resource-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN
Note: Documents which need to make use of APIs that require full cross-origin isolation (such as SharedArrayBuffer
), will also need to serve a Cross-Origin-Embedder-Policy
header, as outlined in Making your website “cross-origin isolated” using COOP and COEP.
Account settings pages, admin panels, and application-specific documents are all good examples of resources which would benefit from as much isolation as possible. For real-life examples, consider:
https://myaccount.google.com/
Not every document that requires sign-in can be fully-isolated from the rest of the internet. It‘s often the case that partial isolation is a better fit. Consider sites that depend upon cross-origin windows for federated workflows involving payments or sign-in, for example. These pages would generally benefit from restricting attackers’ ability to embed them, or obtain their window handle, but they can't easily lock themselves off from all such vectors via Cross-Origin-Opener-Policy: same-origin
and X-Frame-Options: DENY
. In these cases, something like the following set of response headers might be appropriate:
Cross-Origin-Opener-Policy: same-origin-allow-popups Cross-Origin-Resource-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN
The only difference between this case and the “Fully-Isolated” case above is the Cross-Origin-Opener-Policy
value. same-origin
will break the opener relationship between the document and any cross-origin window, regardless of who opened whom. same-origin-allow-popups
will break cross-origin opener relationships initiated by a cross-origin document's use of window.open()
, but will allow the asserting document to open cross-origin windows that retain an opener relationship.
Federated sign-in forms and payment providers are clear examples of documents which intend to be opened by cross-origin windows, and require that relationship to be maintained in order to facilitate communication via channels like postMessage()
or navigation. These documents cannot isolate themselves completely, but can prevent themselves from being embedded or fetched cross-origin. Three scenarios are worth considering:
Documents that only wish to be opened in cross-origin popups could loosen their cross-origin opener policy by serving the following headers:
Cross-Origin-Resource-Policy: same-origin Cross-Origin-Opener-Policy: unsafe-none Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN
For example:
TODO: Find some links.
Documents that only wish to be framed in cross-origin contexts could loosen their framing protections by serving the following headers:
Cross-Origin-Resource-Policy: same-origin Cross-Origin-Opener-Policy: same-origin Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: ALLOWALL
Note that this allows embedding by any cross-origin documents. That's reasonable for some widgety use cases, but when possible, a more secure alternative would specify a list of origins which are allowed to embed the document via the frame-ancestors
CSP directive. That is, in addition to the X-Frame-Options
header above, the following header could also be included to restrict the document to a short list of trusted embedders:
Content-Security-Policy: frame-ancestors https://trusted1.example https://trusted2.example
For example:
TODO: Find some links.
Documents that support both popup and framing scenarios need to loosen both their cross-origin opener policies and framing protections by combining the recommendations above, serving the following headers:
Cross-Origin-Resource-Policy: same-origin Cross-Origin-Opener-Policy: unsafe-none Vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site X-Content-Type-Options: nosniff X-Frame-Options: ALLOWALL
For example:
TODO: Find some links.
Several recommendations above suggest that developers would be well-served to set headers like X-Frame-Options: ALLOWALL
or Cross-Origin-Opener-Policy: unsafe-none
on responses. These map to the web's status quo behavior, and seem therefore superfluous. Why should developers set them?
The core reason is that these defaults are poor fits for today‘s threats, and we ought to be working to change them. Proposals like Embedding Requires Opt-In and COOP By Default suggest that we should shift the web’s defaults away from requiring developers to opt-into more secure behaviors by making them opt-out rather than opt-in. This would place the configuration cost on those developers whose projects require risky settings.
This document recommends setting those less-secure header values explicitly, as that makes it more likely that we‘ll be able to shift the web’s defaults in the future.
This document relies upon a number of excellent resources that spell out much of the foundation of our understanding of Spectre's implications for the web, and justify the mitigation strategies we currently espouse. The following is an incomplete list of those works, in no particular order:
Chris Palmer's Isolating Application-Defined Principles
Charlie Reis' Long-Term Web Browser Mitigations for Spectre
Anne van Kesteren's A Spectre-Shaped Web, Safely Reviving Shared Memory, and Opaque Resource Blocking.
Artur Janc and Mike West's How do we stop spilling the beans across origins?
Mike West's Cross-Origin Embedder Policy explainer
Charlie Reis and Camille Lamy's Cross-Origin Opener Policy explainer
Artur Janc, Charlie Reis, and Anne van Kesteren's COOP and COEP Explained
Artur Janc's Notes on the threat model of cross-origin isolation