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.
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.
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.
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.
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.