Fetch Metadata and Isolation Policies
Tags: security, webdev, programming, cybersecurity
Published: 2021-03-30 19:47:03

Learn everything about the fetch metadata headers and how you can implement isolation policies to defend against various client-side attacks. You can read the original article [here](https://www.appsecmonkey.com/blog/fetch-metadata).

## What are fetch metadata headers?

Fetch metadata headers are a relatively new browser security feature that you can use in your web application to protect against client-side attacks like never before.

The purpose of the headers is simple. They tell the web server about the context in which an HTTP request happened.

### An example

Here's Jim. He is logged in at `b.example` with the cookie `SessionId=123`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uar40zjh30z6c2qsdqqd.png)

Say that there's a website `a.example` that loads an image from `b.example`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51f3y8llro6nakhyur9e.png)

### Without fetch metadata headers

Before the fetch metadata headers existed, all the webserver saw when Jim opened `a.example` was that someone with Jim's cookie loaded the image.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fmedoxnhbvg0qrjg9cgk.png)

On HTTP level it might look like this:

```yaml
GET /cat.jpg HTTP/1.1
Host: b.example
Cookie: SessionId=123
```

Note that the server has no way to know whether it's `a.example` or `b.example` loading the image with Jim's session ID.

### With fetch metadata headers

With fetch metadata headers supported, the browser will send more information in the request to the webserver.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vosnt26s1vn9mt7h7wib.png)

On HTTP level, it might look like this:

```yaml
GET /cat.jpg HTTP/1.1
Host: b.example
Cookie: SessionId=123
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: cross-site
```

Notice how the browser told the webserver three new things this time:

1. The request originated from an image element.
2. The request's [mode](https://developer.mozilla.org/en-US/docs/Web/API/Request/mode) (that this was a resource load as opposed to, e.g., navigation).
3. The request originated from another website.

Actually there is a fourth thing implied in the request as well. The lack of `Sec-Fetch-User` header implies that the request didn't originate as a result of user interaction.

## What attacks fetch metadata headers prevent?

Fetch metadata headers can _help_ prevent pretty much any cross-site attack. These include:

- [XSS (Cross-Site Scripting) Attacks](https://appsecmonkey.com/blog/xss)
- [CSRF (Cross-Site Request Forgery) Attacks](https://appsecmonkey.com/blog/csrf)
- [Various XS-leaks](https://appsecmonkey.com/blog/xs-leaks)
- [XSSI (Cross-Site Script Inclusion) Attacks](https://portswigger.net/research/json-hijacking-for-the-modern-web)
- [Spectre](https://developers.google.com/web/updates/2018/02/meltdown-spectre)
- [Clickjacking](https://portswigger.net/web-security/clickjacking)

## Fetch metadata headers

The system consists of four HTTP request headers.

### Sec-Fetch-Site

The `Sec-Fetch-Site` header tells the webserver if the request originated from the same origin, the same site, or a different website entirely. It can have one of the following values:

- `same-origin`: The request came from the same _origin_, that is, the _host_, _port_, and _scheme_ were identical. You can read more about what origin means [here](https://appsecmonkey.com/blog/same-origin-policy).
- `same-site`: The request came from a different origin but from the same _site_, which is browser lingo for _subdomain of the same registrable domain_. For example: `https://a.example.com` and `https://b.example.com` are considered same-site (but different origin).
- `cross-site`: The request came from a different website completely. For example, `www.google.com` and `www.facebook.com` are considered cross-site.
- `none`: The request didn't originate from any website. This happens when the user opens a bookmark, types manually in the URL bar, opens a link from another program, etc.

### Sec-Fetch-Mode

The `Sec-Fetch-Mode` tells the webserver the request's [mode](https://developer.mozilla.org/en-US/docs/Web/API/Request/mode). It can have one of the following values:

- `same-origin`: The browser is making a request to the same origin. Requests using this mode will fail if the target is of a different origin.
- `no-cors`: The browser is making a request to another origin but doesn't expect to read the response or use any non-safelisted HTTP verbs or headers.
- `cors`: The browser attempts to make a CORS (Cross-Origin Resource Sharing) request. You can read more about CORS [here](https://appsecmonkey.com/blog/cors).
- `navigate`: The browser is navigating from one page to another, such as when clicking a link, receiving a redirect, or opening a bookmark.

### Sec-Fetch-Dest

The `Sec-Fetch-Dest` tells the webserver the request's destination, that is, what kind of place is waiting for the resource. In the example above, it was an `<img>` element loading the resource, so the destination was `image`.

Some of the possible values are:

- `empty`: When the resource is loaded via `fetch()` or XHR.
- `document`: When the resource is loaded in top-level navigation.
- `image`: When the resource is loaded in an `<img>` tag.
- `worker`: When the resource is loaded via `new Worker(...)`.
- `iframe`: When the resource is loaded into an `<iframe>`.

The value can be any element capable of loading an external resource, so also `audio`, `audioworklet`, `embed`, `font`, `frame`, `manifest`, `object`, `paintworklet`, `report`, `script`, `serviceworker`, `sharedworker`, `style`, `track`, `video`, `xslt`, etc. are possible.

### Sec-Fetch-User

The `Sec-Fetch-User` header tells the webserver a navigation request originated (in theory) due to user interaction, such as by clicking a link. The value is always `?1`. When navigation occurs as a result of something that browsers don't consider "user activation", this header is not sent at all.

## Browser support

At the time of this writing, fetch metadata headers are supported on Chrome, Edge, and Opera. But don't worry, you can implement a policy in a fully backward-compatible way. You can check the up-to-date status [here](https://www.appsecmonkey.com/browser-support/fetch-metadata).

## Implementing an isolation policy

The reason fetch metadata headers are so fantastic is that they allow us to do this.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/513623sjjinovl2v1aje.png)

Blocking a request on the server-side based on the client-side context is a power that we've never had before. But now we do, so let's use it and implement an _isolation policy_.

To create an isolation policy, you need a filter, middleware, etc., that enables you to block requests based on the HTTP request headers.

I will use NodeJS as an example, but such middleware is usually trivial to implement in any development framework.

We'll start with this skeleton:

```js
app.use(function (req, res, next) {})
```

### 1. Backward compatibility

Begin your policy by allowing requests that don't have the fetch metadata headers at all. Otherwise, people using browsers that don't yet support fetch metadata wouldn't be able to access your website.

```js
if (!req.headers['sec-fetch-site']) {
  return next()
}
if (!req.headers['sec-fetch-mode']) {
  return next()
}
if (!req.headers['sec-fetch-dest']) {
  return next()
}
```

### 2. Allow all requests from the same origin

To make your application work properly, allow all interactions from the same origin.

```js
if (req.headers['sec-fetch-site'] === 'same-origin') {
  return next()
}
```

### 3. Allow requests that don't originate from another website

Sometimes requests originate from the user opening a bookmark or typing in the URL bar. In these cases, the value of `Sec-Fetch-Site` is `none`, so let's allow that.

```js
if (req.headers['sec-fetch-site'] === 'none') {
  return next()
}
```

### 4. Allow navigation

To enable other websites to link to your page, navigation has to be allowed. However, just allowing navigation would also allow cross-site POST requests, framing, and other things we don't necessarily want.

To be safe, verify that the HTTP method is `GET` and that the resource destination is `document`.

```js
if (
  req.method === 'GET' &&
  req.headers['sec-fetch-mode'] === 'navigate' &&
  req.headers['sec-fetch-dest'] === 'document'
) {
  return next()
}
```

### 5. Block any other requests

If a request didn't match any rules so far, reject it.

```js
return res.status(403).json({
  error: 'Request blocked by the isolation policy.',
})
```

### 6. Relax the policy if required

You may want to allow some cross-origin or cross-site interactions.

#### Allow subdomains

To allow requests from your own subdomains, let requests through that have the `Sec-Fetch-Site` value of `same-site`.

```js
if (req.headers['sec-fetch-site'] === 'same-site') {
  return next()
}
```

#### Allow framing

To enable other websites to load your page in an iframe, allow navigating `GET` requests when the destination is `iframe`.

```js
if (
  req.method === 'GET' &&
  req.headers['sec-fetch-mode'] === 'navigate' &&
  req.headers['sec-fetch-dest'] === 'iframe'
) {
  return next()
}
```

### 7. Test the policy

We have now implemented a rather strict isolation policy. Here is the entire thing (I didn't allow iframes or subdomains in my example). You can fork and play with the code [here](https://codesandbox.io/s/zen-sea-kngz9?file=/app.js).

```js
app.use(function (req, res, next) {
  // If fetch metadata is not supported, allow the request.
  if (!req.headers['sec-fetch-site']) {
    return next()
  }
  if (!req.headers['sec-fetch-mode']) {
    return next()
  }
  if (!req.headers['sec-fetch-dest']) {
    return next()
  }

  // If the request originates from your own web application, allow it.
  if (req.headers['sec-fetch-site'] === 'same-origin') {
    return next()
  }

  // If the request doesn't originate from a website at all (bookmark, etc.) then allow it.
  if (req.headers['sec-fetch-site'] === 'none') {
    return next()
  }

  // If the request is a navigation GET request, allow it.
  if (
    req.method === 'GET' &&
    req.headers['sec-fetch-mode'] === 'navigate' &&
    req.headers['sec-fetch-dest'] === 'document'
  ) {
    return next()
  }

  // If no rules matched, block the request.
  return res.status(403).json({
    error: 'Request blocked by the isolation policy.',
  })
})
```

I've created a little test page [here](https://lmbz9.csb.app/). It tries to load an image, POST an HTML form and frame the protected website. If you open the site, you will find that all of the three will fail.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a9f7fkcow5b06aese280.png)

There is also a link. Clicking through the link, the page loads correctly.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x83rcpf2pnvpzb8xe7hu.png)

Also, loading the page in Firefox or Safari works fine, so our backward compatibility seems to be in check.

### 8. Deploy the policy

When deploying the policy to production for the first time, it is recommended to use a logging-only approach.

The policy would remain the same, but instead of _blocking_ requests, you log that a request would have been blocked because of X, and then you let the request through.

This way, you will quickly know if the policy would break something and if there's something you have to add before finally deploying the enforcing policy.

## Conclusion

Fetch metadata headers are a remarkable browser feature that enables developers to secure web applications in a way that hasn't previously been possible.

Implementing an effective policy is simple, and it can easily be relaxed to accommodate any special needs, such as allowing framing or interactions from subdomains.

When deploying the policy to production, it is recommended to start with a logging-only approach. Then later deploy the enforcing policy when you're satisfied that it won't break anything.

Browser support is still lacking or experimental depending on the browser, but the headers can already be used in a backward compatible way.

## Get the web security checklist spreadsheet!
[![Subscribe](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iqd1ef4a5a0icyvvk65.jpg)](https://eepurl.com/hqAGt5)
 ☝️ [Subscribe](https://eepurl.com/hqAGt5) to AppSec Monkey's email list, get our best content delivered straight to your inbox, and <b>get our 2021 Web Application Security Checklist Spreadsheet for FREE</b> as a welcome gift!

## Don't stop here
If you like this article, check out the other application security guides we have on [AppSec Monkey](https://www.appsecmonkey.com/) as well.

Thanks for reading.