Referrer-Policy and Permissions-Policy
This reference is part of the Web Security Headers Fundamentals reference and covers two privacy-and-capability headers that ship together in almost every hardened deployment: Referrer-Policy, which governs what the browser puts in the outgoing Referer request header, and Permissions-Policy, which governs which powerful browser APIs a document and its embedded frames may use. They solve different problems — one stops URL metadata from leaking, the other stops hardware and capability abuse — but both are declarative allowlists evaluated by the browser before any page script runs, and both are commonly set side by side at the same edge or origin layer.
The two headers are independent. Referrer-Policy controls an outbound request header; Permissions-Policy controls a per-document capability surface. Neither is a substitute for Content Security Policy, which restricts what resources load, and neither enforces transport security the way HTTP Strict Transport Security does. Treat all four as separate layers.
Threat Model & Protocol Mechanics
Referrer leakage
When a browser navigates or fetches a subresource, it attaches a Referer header (the historical misspelling is baked into the protocol) describing the URL the request originated from. Without a policy, older browser defaults transmitted the full URL — scheme, host, path, and query string — to any same-protocol destination. That is a direct data-exfiltration channel:
- Path segments and query parameters routinely carry session identifiers, password-reset tokens, OAuth
code/statevalues, internal ticket numbers, and PII such as?email=or?user_id=12345. Any of those leak verbatim to third-party analytics, ad networks, embedded widgets, and outbound link targets. - Internal URL structure (
/admin/,/internal/v2/billing/) leaks to external CDNs and font/script hosts, handing an attacker a map of your application surface. - Cross-origin referrers are a tracking primitive: a third party embedded across many sites correlates the
Referervalues to rebuild a user’s browsing path.
The Referrer Policy specification (W3C) defines the eight policy tokens and the exact stripping behaviour for each, including how the policy interacts with protocol downgrade (HTTPS → HTTP). Modern Chromium, Firefox, and Safari default to strict-origin-when-cross-origin, but the default is not guaranteed across privacy-hardened or legacy agents, so the header must be set explicitly for deterministic behaviour.
The stripping is not applied to the stored URL; it happens at the moment the browser composes the outbound request. Under the recommended baseline, a request that leaves your origin for a third party is rewritten so that only the origin survives — the sensitive path and query never leave the machine.
Several behaviours surprise engineers who read only the token names. The “origin” a policy emits is the serialised tuple of scheme, host, and port with a trailing slash (https://app.example.com/), never a bare host; a non-default port such as :8443 is part of that origin and is preserved. A change of scheme from https to http is a downgrade and every strict- token suppresses the Referer entirely on it, but a change from http to https is an upgrade and is not treated as a downgrade, so the header is sent. The policy governs subresource fetches, <img>, <script>, fetch(), form submissions, and <link rel="prefetch"> exactly as it governs top-level navigations — an <img src> pointing at a third-party tracker leaks the same path a link would. A per-request override still exists: an individual element may tighten (never loosen relative to what the document allows in a way that leaks more) its own policy with a referrerpolicy attribute (<a referrerpolicy="no-referrer">), and a rel="noreferrer" link drops the header for that navigation regardless of the document policy. Redirects re-evaluate the policy at each hop, so a same-origin URL that 302-redirects to a third party still has its Referer trimmed to the origin on the cross-origin leg.
Hardware-feature and capability abuse
Permissions-Policy exists because a document — including a third-party <iframe> you embed — can otherwise attempt to use powerful APIs: geolocation, camera, microphone, payment, usb, fullscreen, autoplay, and motion sensors. The threats:
- An embedded ad or widget frame silently requesting
camera/microphoneto capture environmental data, orgeolocationto fingerprint and track. - A compromised third-party script invoking the
paymentrequest API orusb/serialdevice access from within your origin’s trust boundary. - Sensor APIs (accelerometer, gyroscope, magnetometer) used for cross-site fingerprinting that survives cookie clearing.
Permissions-Policy denies these by default per feature and delegates them explicitly. It supersedes the deprecated Feature-Policy header. The Permissions Policy specification (W3C) defines the header as an HTTP structured field — a structured dictionary whose values are allowlists of origins. Getting the structured-field syntax wrong (legacy Feature-Policy whitespace-separated grammar, missing quotes, trailing commas) makes the browser silently ignore the directive, which fails open.
Directive Syntax & Spec
Referrer-Policy
The header value is a single policy token (or a comma-separated list where later tokens are fallbacks for unsupported earlier ones):
Referrer-Policy: strict-origin-when-cross-origin
| Directive | Type | Default | Security Impact | When to Deviate |
|---|---|---|---|---|
no-referrer |
token | — | Sends nothing, ever. Maximum privacy. | Use for high-sensitivity flows; breaks affiliate/attribution. |
no-referrer-when-downgrade |
token | legacy default | Sends full URL except on HTTPS→HTTP. Leaks full path cross-origin. | Documented legacy fallback only. |
origin |
token | — | Always sends only scheme://host:port. |
When any cross-origin attribution by origin is acceptable. |
origin-when-cross-origin |
token | — | Full URL same-origin, origin cross-origin, but still sends on downgrade. | Rarely; the strict variant is safer. |
same-origin |
token | — | Full URL same-origin, nothing cross-origin. | Strict same-origin-only apps. |
strict-origin |
token | — | Origin only, and nothing on downgrade. | When you never need same-origin paths in Referer. |
strict-origin-when-cross-origin |
token | modern default | Full URL same-origin, origin cross-origin, nothing on downgrade. | The recommended baseline. |
unsafe-url |
token | — | Always sends the full URL, including on downgrade. Leaks everything. | Never in production. |
Common gotcha: an unrecognised token causes the browser to fall back to its default policy, not to fail. Always validate the exact spelling. A <meta name="referrer"> tag overrides nothing if the HTTP header is present and parsed — but a malformed header lets the meta tag win.
For the recommended baseline, see setting Referrer-Policy: strict-origin-when-cross-origin, which covers the per-stack rollout for that one value in depth.
Permissions-Policy
The value is a structured-field dictionary. Each entry is feature=<allowlist>, where the allowlist is one of () (deny all), * (allow all origins), (self) (this origin only), or (self "https://trusted.example") (this origin plus named origins, each origin a quoted string). Multiple features are comma-separated:
Permissions-Policy: geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self)
| Directive | Type | Default behaviour | Security Impact | When to Deviate |
|---|---|---|---|---|
geolocation=() |
allowlist | deny all | Blocks location access in document and all frames. | (self) if your own app needs location. |
camera=() |
allowlist | deny all | Blocks getUserMedia video. |
(self) for video-call features. |
microphone=() |
allowlist | deny all | Blocks getUserMedia audio. |
(self) for voice features. |
payment=() |
allowlist | deny all | Blocks the Payment Request API. | (self) or (self "https://checkout.example") for checkout. |
fullscreen=(self) |
allowlist | varies by feature | Restricts requestFullscreen to your origin. |
* only if embedders legitimately need it. |
usb=() / serial=() |
allowlist | deny all | Blocks WebUSB/WebSerial device access. | Almost never deviate. |
Delegation gotcha: omitting a feature from the header does not deny it — each feature has its own spec default (some default-allow for the top document). To guarantee denial you must list the feature with (). The allowlist values are quoted origin strings; bare hostnames or the legacy whitespace-separated Feature-Policy grammar are silently rejected.
Delegation to a child frame is a second, independent gate. Even when a feature appears in the header allowlist, a cross-origin <iframe> cannot use it unless the frame element also carries a matching allow attribute — the two must agree. The evaluation order below shows why a feature can be silently permitted (the omitted-feature case) or silently blocked (the missing allow attribute case).
Two structured-field details trip up hand-written policies. The wildcard is written bare as feature=* (allow every origin) and is not quoted, whereas every concrete origin is quoted — payment=(self "https://checkout.example") — and self is the one keyword that appears unquoted inside the parentheses. A single origin still needs the parentheses: geolocation=("https://maps.example") is valid, but geolocation="https://maps.example" without them is a parse error that discards the whole entry. Because the value is one structured-field dictionary, a syntax error in a single feature does not fail just that feature — a browser that cannot parse the field abandons the entire header, so one stray unquoted origin can silently drop every deny you wrote. Feature names are also not interchangeable with the old Feature-Policy vocabulary: the modern header uses hyphenated identifiers such as clipboard-read, clipboard-write, display-capture, screen-wake-lock, and browsing-topics, and an unknown feature name is ignored rather than rejected, so a typo like microphon=() leaves the microphone at its permissive spec default. Reporting is available: pair Permissions-Policy with a Reporting-Endpoints header and add a report-to group so blocked-feature violations are delivered to your collector, which surfaces frames attempting denied capabilities without waiting for a user complaint. The unload feature is a special case — setting unload=() opts a document out of unload event handlers, which is a prerequisite for back/forward-cache eligibility in Chromium rather than a security control.
() entries are mandatory.Platform-Specific Implementation
Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self)" always;
Security impact: always emits the header on error responses (4xx/5xx) too, closing the gap where an error page would otherwise ship without policy. Note that any add_header inside a location block replaces all inherited add_header directives — repeat both headers in every block that defines its own.
Verify: curl -sI https://example.com | grep -iE 'referrer-policy|permissions-policy'
Apache
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self)"
Security impact: always adds the header to internally generated error responses as well as 2xx; set (vs add) guarantees a single deterministic value and overrides upstream module output. Place in <VirtualHost> or .htaccess with mod_headers enabled.
Verify: curl -sI https://example.com | grep -i permissions-policy
IIS
<system.webServer>
<httpProtocol>
<customHeaders>
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
<add name="Permissions-Policy" value="geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self)" />
</customHeaders>
</httpProtocol>
</system.webServer>
Security impact: customHeaders in web.config applies the header to every response the site returns, including static files served directly by IIS without hitting application code. Use <remove> before <add> if a parent config already declares the header, to avoid duplicates.
Verify: curl -sI https://example.com | findstr /I permissions-policy
Node/Express (Helmet)
Helmet sets Referrer-Policy natively. Permissions-Policy has no built-in Helmet helper, so set it raw:
const helmet = require("helmet");
app.use(
helmet({
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
})
);
app.use((_req, res, next) => {
res.setHeader(
"Permissions-Policy",
"geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self)"
);
next();
});
Security impact: Helmet normalises casing and prevents duplicate Referrer-Policy declarations. Registering the Permissions-Policy middleware before your routes guarantees it runs on every response, including error handlers downstream.
Verify: curl -sI http://localhost:3000 | grep -iE 'referrer-policy|permissions-policy'
Cloudflare
Use a Transform Rule (Rules → Transform Rules → Modify Response Header) to Set both headers, or a Worker for programmatic control:
export default {
async fetch(request) {
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
headers.set(
"Permissions-Policy",
"geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self)"
);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
},
};
Security impact: Edge injection enforces the policy even when the origin is misconfigured, and headers.set (not append) overwrites any origin value so you never ship duplicates. The body stream is preserved untouched.
Verify: curl -sI https://example.com | grep -i referrer-policy
Verification & Diagnostic Workflows
-
Header presence and exact value:
curl -sI https://example.com | grep -iE 'referrer-policy|permissions-policy'Expected:
referrer-policy: strict-origin-when-cross-origin permissions-policy: geolocation=(), camera=(), microphone=(), payment=(self), fullscreen=(self) -
DevTools: Network tab → select the document request → Response Headers. Confirm a single instance of each header (duplicates indicate two layers both setting them).
-
Referrer behaviour: Navigate from your HTTPS origin to a third-party HTTPS page and read
document.referrerin that page’s console. Understrict-origin-when-cross-originexpect onlyhttps://example.com/, never a path. -
Feature denial: In a cross-origin iframe, run
navigator.geolocation.getCurrentPosition(console.log, console.error). Expect aGeolocationPositionErrorwithcode 1(PERMISSION_DENIED) whengeolocation=()is set. -
CI/CD gate:
#!/usr/bin/env bash set -euo pipefail h=$(curl -sI "https://staging.example.com") echo "$h" | grep -iq 'referrer-policy: strict-origin-when-cross-origin' || { echo "Referrer-Policy missing/wrong"; exit 1; } echo "$h" | grep -iq 'permissions-policy:.*geolocation=()' || { echo "Permissions-Policy missing geolocation deny"; exit 1; }
Troubleshooting, Misconfigurations & Safe Rollback
- Header absent in production but present at origin → fix: an edge cache is serving entries stored before the rule existed. Purge the CDN cache and confirm the rule applies to all paths, not just HTML.
Permissions-Policyignored, console parse error → fix: legacyFeature-Policygrammar or unquoted origins. Use the structured-field form:feature=(),feature=(self),feature=(self "https://origin"). No trailing commas.- Feature still works despite expectation of denial → fix: the feature was omitted from the header, so its spec default applied. List it explicitly with
(). - Analytics or affiliate attribution dropped → fix: an over-strict referrer value (
no-referrer) stripped the data. Revert tostrict-origin-when-cross-origin, which preserves origin-level attribution cross-origin, and handle finer attribution server-side. See the rollback steps in setting Referrer-Policy: strict-origin-when-cross-origin. - Embedded media or fullscreen broken → fix:
fullscreen=()or autoplay denial blocked a legitimate player. Grantfullscreen=(self)(or add the embedder origin) rather than disabling the header. - Accessibility/media regression → fix: screen-reader media controls or video conferencing stopped working after a blanket
camera=(), microphone=(). Scope to(self)for the routes that need them instead of a site-wide deny. - Duplicate headers → fix: two layers (framework + proxy) both emit the header. Consolidate to a single layer and remove the other.
- Whole Permissions-Policy stops applying after one edit → fix: a single malformed entry (unquoted origin, stray
*in quotes, trailing comma) invalidates the entire structured field. Validate the full string, not just the feature you touched; every concrete origin must be a quoted string inside(). document.referreris empty on a same-origin page → fix: the referrer originated from anhttps://document that reached the page via anhttp://hop, or the source link carriedrel="noreferrer". Confirm the whole path stays on HTTPS and that no element sets a tighterreferrerpolicy.- Feature blocked only inside an iframe → fix: the header allowlist grants the origin, but the
<iframe>lacks a matchingallowattribute. Addallow="geolocation"(or the relevant feature) to the frame element so both gates agree.
Safe rollback: these headers are non-destructive — reverting is a config change plus a cache purge, with no client-side lock-in (unlike HSTS preload). To roll Referrer-Policy back to the broadest compatible value, set Header always set Referrer-Policy "no-referrer-when-downgrade", purge the CDN, and re-run the curl -sI check. For Permissions-Policy, remove individual feature=() entries to re-enable a capability without dropping the rest of the policy.
Frequently Asked Questions
Does omitting a feature from Permissions-Policy deny it?
No. Each feature has its own spec default, and several default to allow for the top-level document. To guarantee denial you must list the feature explicitly with an empty allowlist, e.g. geolocation=().
Is Permissions-Policy a replacement for Feature-Policy?
Yes. Permissions-Policy supersedes the deprecated Feature-Policy header and uses a different structured-field grammar (quoted origin strings, feature=(allowlist)). Do not ship both; emit only Permissions-Policy.
Will strict-origin-when-cross-origin break my analytics? It preserves the origin cross-origin, so platforms that attribute by referring origin keep working. Tools that rely on full referring URLs lose path detail; move that attribution to first-party parameters or server-side logging.
Do these headers need to be set if browsers already default to them? Yes. Browser defaults are not contractual and differ across privacy-hardened and legacy agents. Explicit headers give deterministic, auditable behaviour and satisfy compliance scans.
Can I set these via a <meta> tag instead?
Referrer-Policy has a <meta name="referrer"> equivalent, but it is overridden by the HTTP header and is weaker. Permissions-Policy has no meta equivalent. Set both as HTTP response headers.
Does the port number appear in the Referer under origin-only policies?
Yes. The emitted origin is the full scheme-host-port tuple, so a service on https://app.example.com:8443 sends https://app.example.com:8443/ as the referrer. Only the default port for the scheme is omitted.
Why is my feature still allowed after I set feature=*?
* allows every origin, so it grants rather than denies. The wildcard is written bare (no quotes). To deny a feature everywhere use the empty allowlist feature=().
Related
-
Web Security Headers Fundamentals — the parent reference for all header layers.
-
Setting Referrer-Policy: strict-origin-when-cross-origin — per-stack rollout of the recommended value.
-
Permissions-Policy: disabling browser features — denying camera, geolocation, and other capabilities.
-
Content Security Policy Essentials — restricting which resources load.
-
Cross-Origin Isolation: COOP, COEP, CORP — isolating your origin from cross-origin documents.
-
Migrating Feature-Policy to Permissions-Policy — the structured-field syntax change.
-
Referrer-Policy values compared — what each of the eight values leaks.