Protect your resources from web attacks with Fetch Metadata

Prevent CSRF, XSSI, and cross-origin information leaks.

Lukas Weichselbaum
Lukas Weichselbaum

Many web applications are vulnerable to cross-origin attacks like cross-site request forgery (CSRF), cross-site script inclusion (XSSI), timing attacks, cross-origin information leaks or speculative execution side-channel (Spectre) attacks.

Fetch Metadata request headers allow you to deploy a strong defense-in-depth mechanism—a Resource Isolation Policy—to protect your application against these common cross-origin attacks.

It is common for resources exposed by a given web application to only be loaded by the application itself, and not by other websites. In such cases, deploying a Resource Isolation Policy based on Fetch Metadata request headers takes little effort, and at the same time protects the application from cross-site attacks.

Browser compatibility

Fetch Metadata request headers are supported in all modern browser engines.

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 90.
  • Safari: 16.4.

Source

Background

Many cross-site attacks are possible because the web is open by default and your application server cannot easily protect itself from communication originating from external applications. A typical cross-origin attack is cross-site request forgery (CSRF) where an attacker lures a user onto a site they control and then submits a form to the server the user is logged in to. Since the server cannot tell if the request originated from another domain (cross-site) and the browser automatically attaches cookies to cross-site requests, the server will execute the action requested by the attacker on behalf of the user.

Other cross-site attacks like cross-site script inclusion (XSSI) or cross-origin information leaks are similar in nature to CSRF and rely on loading resources from a victim application in an attacker-controlled document and leaking information about the victim applications. Since applications cannot easily distinguish trusted requests from untrusted ones, they cannot discard malicious cross-site traffic.

Introducing Fetch Metadata

Fetch Metadata request headers are a new web platform security feature designed to help servers defend themselves against cross-origin attacks. By providing information about the context of an HTTP request in a set of Sec-Fetch-* headers, they allow the responding server to apply security policies before processing the request. This lets developers decide whether to accept or reject a request based on the way it was made and the context in which it will be used, making it possible to respond to only legitimate requests made by their own application.

Same-Origin
Requests originating from sites served by your own server (same-origin) will continue to work. A fetch request from https://site.example for the resource https://site.example/foo.json in JavaScript causes the browser to send the HTTP request header 'Sec Fetch-Site: same-origin'.
Cross-site
Malicious cross-site requests can be rejected by the server because of the additional context in the HTTP request provided by Sec-Fetch-* headers. An image on https://evil.example that has set the src attribute of an img element to 'https://site.example/foo.json' causes the browser to send the HTTP request header 'Sec-Fetch-Site: cross-site'.

Sec-Fetch-Site

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 90.
  • Safari: 16.4.

Source

Sec-Fetch-Site tells the server which site sent the request. The browser sets this value to one of the following:

  • same-origin, if the request was made by your own application (e.g. site.example)
  • same-site, if the request was made by a subdomain of your site (e.g. bar.site.example)
  • none, if the request was explicitly caused by a user's interaction with the user agent (e.g. clicking on a bookmark)
  • cross-site, if the request was sent by another website (e.g. evil.example)

Sec-Fetch-Mode

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 90.
  • Safari: 16.4.

Source

Sec-Fetch-Mode indicates the mode of the request. This roughly corresponds to the type of the request and allows you to distinguish resource loads from navigation requests. For example, a destination of navigate indicates a top-level navigation request while no-cors indicates resource requests like loading an image.

Sec-Fetch-Dest

Browser Support

  • Chrome: 80.
  • Edge: 80.
  • Firefox: 90.
  • Safari: 16.4.

Source

Sec-Fetch-Dest exposes a request's destination (e.g. if a script or an img tag caused a resource to be requested by the browser).

How to use Fetch Metadata to protect against cross-origin attacks

The extra information these request headers provide is quite simple, but the additional context allows you to build powerful security logic on the server-side, also referred to as a Resource Isolation Policy, with just a few lines of code.

Implementing a Resource Isolation Policy

A Resource Isolation Policy prevents your resources from being requested by external websites. Blocking such traffic mitigates common cross-site web vulnerabilities such as CSRF, XSSI, timing attacks, and cross-origin information leaks. This policy can be enabled for all endpoints of your application and will allow all resource requests coming from your own application as well as direct navigations (via an HTTP GET request). Endpoints that are supposed to be loaded in a cross-site context (e.g. endpoints loaded using CORS) can be opted out of this logic.

Step 1: Allow requests from browsers which don't send Fetch Metadata

Since not all browsers support Fetch Metadata, you need to allow requests that don't set Sec-Fetch-* headers by checking for the presence of sec-fetch-site.

if not req['sec-fetch-site']:
 
return True  # Allow this request

Step 2: Allow same-site and browser-initiated requests

Any requests that do not originate from a cross-origin context (like evil.example) will be allowed. In particular, these are requests that:

  • Originate from your own application (e.g. a same-origin request where site.example requests site.example/foo.json will always be allowed).
  • Originate from your subdomains.
  • Are explicitly caused by a user's interaction with the user agent (e.g. direct navigation or by clicking a bookmark, etc.).
if req['sec-fetch-site'] in ('same-origin', 'same-site', 'none'):
 
return True  # Allow this request

Step 3: Allow simple top-level navigation and iframing

To ensure that your site can still be linked from other sites, you have to allow simple (HTTP GET) top-level navigation.

if req['sec-fetch-mode'] == 'navigate' and req.method == 'GET'
 
# <object> and <embed> send navigation requests, which we disallow.
 
and req['sec-fetch-dest'] not in ('object', 'embed'):
   
return True  # Allow this request

Step 4: Opt out endpoints that are meant to serve cross-site traffic (Optional)

In some cases, your application might provide resources which are meant to be loaded cross-site. These resources need to be exempted on a per-path or per-endpoint basis. Examples of such endpoints are:

  • Endpoints meant to be accessed cross-origin: If your application is serving endpoints that are CORS enabled, you need to explicitly opt them out from resource isolation to ensure that cross-site requests to these endpoints are still possible.
  • Public resources (e.g. images, styles, etc.): Any public and unauthenticated resources that should be loadable cross-origin from other sites can be exempted as well.
if req.path in ('/my_CORS_endpoint', '/favicon.png'):
 
return True

Step 5: Reject all other requests that are cross-site and not navigational

Any other cross-site request will be rejected by this Resource Isolation Policy and thus protect your application from common cross-site attacks.

Example: The following code demonstrates a complete implementation of a robust Resource Isolation Policy on the server or as a middleware to deny potentially malicious cross-site resource requests, while allowing simple navigational requests:

# Reject cross-origin requests to protect from CSRF, XSSI, and other bugs
def allow_request(req):
 
# Allow requests from browsers which don't send Fetch Metadata
 
if not req['sec-fetch-site']:
   
return True

 
# Allow same-site and browser-initiated requests
 
if req['sec-fetch-site'] in ('same-origin', 'same-site', 'none'):
   
return True

 
# Allow simple top-level navigations except <object> and <embed>
 
if req['sec-fetch-mode'] == 'navigate' and req.method == 'GET'
   
and req['sec-fetch-dest'] not in ('object', 'embed'):
     
return True

 
# [OPTIONAL] Exempt paths/endpoints meant to be served cross-origin.
 
if req.path in ('/my_CORS_endpoint', '/favicon.png'):
   
return True

 
# Reject all other requests that are cross-site and not navigational
 
return False

Deploying a Resource Isolation Policy

  1. Install a module like the code snippet from above to log and monitor how your site behaves and make sure the restrictions don't affect any legitimate traffic.
  2. Fix potential violations by exempting legitimate cross-origin endpoints.
  3. Enforce the policy by dropping non-compliant requests.

Identifying and fixing policy violations

It's recommended that you test your policy in a side-effect free way by first enabling it in reporting mode in your server-side code. Alternatively, you can implement this logic in middleware, or in a reverse proxy which logs any violations that your policy might produce when applied to production traffic.

From our experience of rolling out a Fetch Metadata Resource Isolation Policy at Google, most applications are by default compatible with such a policy and rarely require exempting endpoints to allow cross-site traffic.

Enforcing a Resource Isolation Policy

After you've checked that your policy doesn't impact legitimate production traffic, you're ready to enforce restrictions, guaranteeing that other sites will not be able to request your resources and protecting your users from cross-site attacks.

Further reading