Isolating your web resources from cross-origin attacks

Cross-origin attacks—CSRF, cross-site script inclusion (XSSI), timing attacks, and cross-origin information leaks—remain a persistent problem for web applications. The root cause is that the web platform is open by default: servers often can't distinguish a legitimate request from their own application from a malicious request originating on another site. Fetch Metadata request headers give servers the context they need to close that gap.

Fetch Metadata is a set of Sec-Fetch-* request headers that describe how an HTTP request was made and where it will be used. By checking these headers on the server before processing a request, you can deploy a Resource Isolation Policy that blocks cross-site requests to resources that are only meant to be loaded by your own application. All modern browser engines support these headers, so the mechanism is widely available.

What the Sec-Fetch-* headers tell you

Three headers carry the information needed to evaluate a request:

  • Sec-Fetch-Site — the site that sent the request. Values are same-origin (your own application), same-site (a subdomain of your site), cross-site (another origin), or none (the request was caused by direct user interaction, such as a bookmark click).
  • Sec-Fetch-Mode — the request mode, roughly corresponding to the request type. For example, navigate means a top-level navigation, while no-cors marks resource loads like image fetches.
  • Sec-Fetch-Dest — the request destination, such as script or img, telling you which browser feature triggered the request.

Building a resource isolation policy

A Resource Isolation Policy rejects any request that originates from a cross-site context, except for simple navigation. This mitigates CSRF, XSSI, timing attacks, and information leaks without blocking your own application's traffic. Implementation is straightforward and can be done in application code, middleware, or a reverse proxy.

Allow browsers that don't send Fetch Metadata

Because not every browser sends Sec-Fetch-* headers, start by allowing any request that lacks the sec-fetch-site header. This avoids breaking older clients.

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

Allow same-site and browser-initiated requests

Next, permit requests that aren't cross-site: requests from your own origin, requests from your subdomains, and requests caused directly by user interaction with the user agent (for example, clicking a bookmark).

if req['sec-fetch-site'] in ('same-origin', 'same-site', 'none'):
  return True  # Allow this request

Allow top-level navigation

Your site should remain linkable from other sites. Allow simple GET top-level navigations so that users can still reach your pages when they're linked externally.

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

Opt out endpoints that must serve cross-site traffic

Some resources are meant to be loaded cross-origin. Exempt these on a per-path or per-endpoint basis. Typical cases include:

  • CORS-enabled endpoints that other origins consume.
  • Public, unauthenticated assets such as images or styles that other sites may embed.
if req.path in ('/my_CORS_endpoint', '/favicon.png'):
  return True

Reject everything else

Any remaining request that is cross-site and non-navigational is rejected. The full policy, combining all these steps, denies malicious cross-site requests while leaving legitimate traffic untouched:

# 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 the policy

Before enforcing a Resource Isolation Policy, it's wise to roll it out gradually:

  1. Run the policy in a logging or reporting mode to monitor its effects on real traffic and confirm no legitimate requests are blocked.
  2. Fix violations by exempting the legitimate cross-origin endpoints that were flagged.
  3. Switch to enforcement mode, dropping non-compliant requests.

Experience from large-scale deployments shows that most applications are compatible with such a policy out of the box; exemptions are rarely needed.