Permissions-Policy and Cross-Origin Headers

Content Security Policy and HSTS get most of the attention in static site hardening, and rightly so. A second group of headers is cheaper to adopt and often skipped: Permissions-Policy, which switches off browser features the site never uses, and the cross-origin family — Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy and Cross-Origin-Embedder-Policy — which decide how the site's pages and files interact with other origins. For a static documentation site, most of these can be set to their strictest values in minutes. One of them, COEP, can break embeds and third-party assets in confusing ways if set without care.

This guide sets all four on a static documentation site with a few third-party embeds (YouTube via a facade, a Mermaid-free diagram library, an interactive WebAssembly demo on one page), verifies them with browser reporting and security scanners, and shows where each one can break things. It is part of Security Headers and Hardening for Static Sites, alongside Writing a Content Security Policy for a Static Site.

Prerequisites

  • A static site whose response headers you control (_headers, vercel.json, a CDN rule, or server config).
  • An inventory of embeds and third-party resources the site loads.
  • A reporting endpoint for browser violation reports (optional but recommended).

Permissions-Policy: Deny What You Do Not Use

Permissions-Policy lists features and the origins allowed to use them. An empty allowlist () disables the feature for the page and every iframe inside it. A documentation site uses none of the sensitive features:

Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), bluetooth=(), camera=(), display-capture=(),
  geolocation=(), gyroscope=(), hid=(), magnetometer=(), microphone=(), midi=(), payment=(), publickey-credentials-get=(),
  serial=(), usb=(), xr-spatial-tracking=(), browsing-topics=(), interest-cohort=(), fullscreen=(self "https://www.youtube-nocookie.com")

fullscreen is allowed for the site itself and the YouTube embed domain, so videos loaded through the facade can go full screen; everything else is off. Header values must be on one line in most host configuration formats; the line breaks above are for readability.

With these features denied, a compromised third-party script or a malicious iframe cannot prompt readers for camera, location or payment access, and embedded ad or analytics frames cannot read motion sensors for fingerprinting. The header costs nothing in performance.

Permissions-Policy decisions for common browser features A matrix of features. Camera, microphone, geolocation, payment, USB, serial, HID, Bluetooth and motion sensors are denied to all origins. Autoplay is denied. Fullscreen is allowed for the site and the YouTube embed domain. Topics and interest cohort advertising APIs are denied. Browser features the documentation site allows Feature group Policy Reason Camera, microphone, display capture denied () never used Geolocation, sensors denied () blocks fingerprinting Payment, USB, serial, HID denied () never used Fullscreen self + YouTube video facade Topics, interest cohort denied () no ad targeting
The allowlist is derived from what the site actually uses; everything else is off.

Cross-Origin-Opener-Policy: Cut Ties to Other Windows

When a page opens a link in a new window, or is opened by another site, the two windows can hold references to each other (window.opener), which enables attacks such as tab-nabbing and some cross-window leaks. COOP severs the link:

Cross-Origin-Opener-Policy: same-origin

With same-origin, cross-origin windows lose their reference to the site's window and vice versa. The one thing it can break is a flow that deliberately relies on the opener — typically an OAuth or payment popup that posts a message back to the opening window. The documentation site had one: a "Sign in to try the API" popup. For that single page, the policy is relaxed to same-origin-allow-popups, which keeps the link for popups the page itself opens:

/api-explorer/*
  Cross-Origin-Opener-Policy: same-origin-allow-popups

Cross-Origin-Resource-Policy: Keep Your Files Yours

CORP tells browsers which origins may load a resource in no-cors mode (plain <img>, <script>, <link> from another site). For HTML, scripts and styles, same-site is a safe default: the site's own pages and subdomains can load them, other sites cannot embed them. Assets meant to be embedded elsewhere — the logo used in partner pages, images in the RSS feed that feed readers display — need cross-origin:

/*
  Cross-Origin-Resource-Policy: same-site
/img/brand/*
  Cross-Origin-Resource-Policy: cross-origin
/feed-images/*
  Cross-Origin-Resource-Policy: cross-origin

Most feed readers fetch images server-side, where CORP does not apply, but browser-based readers display them directly, so feed images were set to cross-origin after two readers reported broken images in a web-based feed reader.

Effect of Cross-Origin-Resource-Policy on image loads from different origins A page on docs.example.com loads an image from assets.example.com with CORP same-site, which is allowed because both are on example.com. A third-party site tries to embed the same image and the browser blocks it. A browser-based feed reader loads a feed image marked cross-origin, which is allowed. Who may load a CORP-protected image docs.example.com own page assets.example. com CORP: same-site other-site.com third party Web feed reader browser-based load /img/diagram.avif (allowed) embed /img/diagram.avif (blocked) load /feed-images/x.avif (cross-origin, allowed)
CORP is enforced by the browser for no-cors loads; it does not affect server-side fetches such as crawlers.

Cross-Origin-Embedder-Policy: Only Where Isolation Is Needed

COEP with require-corp makes the page refuse to load any cross-origin resource that does not explicitly opt in with CORP or CORS. Combined with COOP same-origin, it makes the page cross-origin isolated, which unlocks SharedArrayBuffer and precise timers — needed by one page on this site, an interactive WebAssembly demo that uses threads.

Applied globally, COEP broke three things in testing: the YouTube embed (YouTube frames do not send CORP), a third-party font from a CDN without CORS headers, and avatar images on the community page loaded from GitHub. So COEP is set only on the demo page, which loads nothing cross-origin:

/playground/wasm-threads/*
  Cross-Origin-Embedder-Policy: require-corp
  Cross-Origin-Opener-Policy: same-origin

credentialless is an alternative COEP value that loads cross-origin no-cors resources without cookies instead of blocking them, which makes isolation easier on pages with images from other origins. The page checks crossOriginIsolated === true in JavaScript and shows a message if isolation is not in effect.

Verifying and Reporting

After deploying, three checks confirm the headers:

  • The redirect and header test table gains rows for each header and path, as in Testing Redirects and Headers Before Deploy.
  • Mozilla Observatory and securityheaders.com scans score the production home page; the site went from a C to an A+ on securityheaders.com.
  • The Reporting API (Report-To / Reporting-Endpoints headers with report-to in COOP and COEP report-only variants) sends violation reports to the same endpoint as CSP reports. Deploying COEP first as Cross-Origin-Embedder-Policy-Report-Only on the demo page showed zero violations before switching to enforcement.

Rolling Out Safely

The order of rollout mattered more than the values. Permissions-Policy went first, site-wide, because denying unused features cannot break anything the site does — the only risk is a future feature that needs one, which the header table in CI makes visible when it fails. CORP and COOP followed a week later on a preview deploy, with the full end-to-end suite and a manual pass through every embed, popup and cross-site link, and then on production with the exceptions described above. COEP stayed report-only for two weeks on the demo page before enforcement, and was never applied elsewhere.

Each header also got a short comment in the headers configuration explaining its purpose and exceptions. Six months later, a developer adding a map embed saw the Permissions-Policy comment, allowed geolocation for the map provider's origin on that one page rather than site-wide, and documented why — which is exactly the behaviour the comments were meant to produce. Headers without explanations tend to get deleted by the next person who finds them in the way.

Measured Impact

MeasureBeforeAfter
securityheaders.com gradeCA+
Sensitive browser features available to embedded framesallnone (fullscreen only where needed)
Pages with COOP0all (relaxed on the API explorer)
Assets embeddable by other sitesallbrand and feed images only
Cross-origin isolated pages01 (WASM demo)
Breakages found in testing before rollout—5 (3 COEP, 1 COOP, 1 CORP)
Breakages reported by readers after rollout—0
Number of issues each header caused during testing before rollout Vertical bars. COEP require-corp applied globally caused 3 breakages, a YouTube embed, a third-party font and GitHub avatar images. COOP same-origin caused 1, the sign-in popup on the API explorer. CORP same-site caused 1, feed images in a web feed reader. Permissions-Policy caused none. Breakages found in testing, by header 3 COEP require-corp 1 COOP same-origin 1 CORP same-site 0 Permissions-Policy
All five were found with report-only headers on a preview deploy before any enforcement in production.

Pitfalls & Rollback

  • COEP everywhere. Breaks third-party embeds and assets; apply it only where isolation is needed.
  • COOP on popup-based sign-in flows. Use same-origin-allow-popups on those pages.
  • CORP on assets meant for others. Brand images and feed images need cross-origin.
  • Multi-line header values. Most host configs need each header on one line.
  • Rollback: headers are configuration; removing a header rule restores the previous behaviour on the next deploy, and report-only variants let you test changes without enforcement.

Conclusion

Permissions-Policy, COOP and CORP are low-risk, high-value headers for static sites: deny every browser feature the site does not use, sever links to cross-origin windows, and keep your files from being embedded where they should not be. COEP is the exception — powerful and easy to break things with — so reserve it for the pages that genuinely need cross-origin isolation, test with report-only first, and ship everything else site-wide.

FAQ

What does Permissions-Policy do on a static site?

It tells the browser which powerful features, such as camera, microphone, geolocation, payment or USB, the page and its embedded iframes may use. A static documentation or marketing site usually needs none of them, so denying them all limits what injected or third-party code could do.

What is Cross-Origin-Opener-Policy for?

It controls whether pages opened from or opening your site share a browsing context group. same-origin severs the link to cross-origin windows, which protects against cross-window attacks and is a prerequisite for cross-origin isolation.

Do I need Cross-Origin-Embedder-Policy?

Only if the site needs cross-origin isolation, for example to use SharedArrayBuffer or high-resolution timers in a WebAssembly demo. COEP require-corp can break third-party images, fonts and iframes that do not send CORP or CORS headers, so most static sites should not set it globally.

What does Cross-Origin-Resource-Policy protect?

It lets a site say which origins may load its resources, such as images, scripts and fonts, in no-cors mode. same-site or same-origin prevents other sites from embedding those resources and reduces exposure to side-channel attacks.

Filed underHow-tos