1. The Support Ticket That Started This
A client forwarded me a screenshot from their affiliate network's dashboard. It was a list of referring URLs — the pages that had sent traffic to the affiliate's tracking link. Most of them were what you'd expect: category pages, a blog post, the homepage. Three rows down was this:
https://shop.example.com/checkout/success?order=100047821&email=priya.n%40gmail.com&total=8499.00
The affiliate network could see a real customer's order number, email address, and order value. So could every other third party whose script or image sat on that success page. Nobody had been hacked. No database had leaked. The browser had simply done what browsers do: attached the current page's full URL to every outbound request as the Referer header, and the success page happened to carry personal data in its query string.
This is one of the most common privacy defects I find in ecommerce, and it is also one of the cheapest to fix. A single response header, correctly chosen, closes the whole category. The catch — and the reason this article runs long — is that the same header also governs how your analytics attribute revenue, whether your affiliate partners get paid, and whether your CDN's hotlink protection keeps working. Set it carelessly and you trade a privacy bug for a reporting outage, then spend a week arguing with marketing about where the traffic went.
So this is not really a five-minute config change. It's a five-minute config change wrapped in two weeks of measurement. Let's do both properly.
2. What the Referer Header Actually Is
When a browser navigates from page A to page B, or loads a subresource on page A, it may attach a header naming where the request came from:
GET /partner-landing HTTP/1.1
Host: affiliate.example.net
Referer: https://shop.example.com/checkout/success?order=100047821
Yes, it's spelled wrong. The original HTTP specification misspelled "referrer" and the typo was baked in before anyone noticed. The header is Referer; the policy that controls it is Referrer-Policy, spelled correctly. The JavaScript property is document.referrer, also correct. You will typo this repeatedly. Everyone does.
The header is sent in more situations than most people assume. It is attached to link clicks, form submissions, image and script and stylesheet loads, XHR and fetch calls, iframe loads, and font requests. Every one of those is an opportunity for the current URL to travel somewhere you didn't intend.
What it is not: authentication, and never a security control. The header is trivially forgeable by any non-browser client, so treating its presence as proof of anything is a mistake I still see in CSRF defences and "hotlink protection" that a single curl -H defeats. Referrer data is a hint about provenance, useful for analytics and nothing stronger.
Historically browsers sent the full URL by default, cross-site included. That changed. Chrome 85 shipped strict-origin-when-cross-origin as the default in August 2020, and Firefox and Safari landed in roughly the same place. Which means: if you have not set this header at all, modern browsers are already protecting you reasonably well. That is worth knowing before you panic. It is also not a reason to skip setting it explicitly, for reasons I'll get to.
3. The Eight Policy Values, Honestly Described
The specification defines eight values. Most articles list them and move on. The differences between the middle four are where every real decision lives, so it's worth being precise about what each one does in the three cases that matter: same-origin requests, cross-origin requests, and downgrades from HTTPS to HTTP.
| Policy | Same-origin | Cross-origin | HTTPS → HTTP |
|---|---|---|---|
| no-referrer | nothing | nothing | nothing |
| no-referrer-when-downgrade | full URL | full URL | nothing |
| origin | origin only | origin only | origin only |
| origin-when-cross-origin | full URL | origin only | origin only |
| same-origin | full URL | nothing | nothing |
| strict-origin | origin only | origin only | nothing |
| strict-origin-when-cross-origin | full URL | origin only | nothing |
| unsafe-url | full URL | full URL | full URL |
"Origin only" means the scheme, host, and port with a trailing slash — https://shop.example.com/ — and nothing else. No path, no query string, no fragment. That distinction is the entire privacy story. Your order ID lives in the query string; your customer's email lives in the query string. Strip the path and query and the leak is gone while the analytics signal "this came from shop.example.com" survives.
A few practical notes on the ones people misuse:
no-referrer is the maximalist choice and it is usually wrong for a commercial site. It breaks your own internal funnel analysis, breaks affiliate attribution completely, and breaks any partner integration that keys off provenance. I reserve it for specific pages — password reset, account recovery, anything where the URL itself is a bearer token — rather than applying it site-wide.
origin looks safe but has a subtle flaw: it still sends the origin on a downgrade to plain HTTP, which means an attacker on the network learns which site the user is browsing. strict-origin is the same policy with that hole closed. There is essentially no reason to choose origin over strict-origin in 2026. Same logic makes origin-when-cross-origin strictly worse than strict-origin-when-cross-origin.
no-referrer-when-downgrade was the old web default and it is the one I most often find still configured on ecommerce sites, usually copied from a 2016 blog post. It sends the full URL to every third party over HTTPS. It is precisely the policy that produced the screenshot at the top of this article. If you find this in your config, that is your bug.
unsafe-url does what the name says. I have used it exactly once, on an internal tool behind a VPN where a legacy partner system needed the full path and the data was non-sensitive. If you are reaching for it on a public storefront, the thing you actually want is a per-element override, covered under per-element control below.
4. Where Ecommerce Actually Leaks
Abstract privacy arguments don't move roadmaps. Specific URLs do. Here are the leaks I find most often during audits, roughly in order of how much trouble they cause.
Order confirmation pages
The worst offender by a wide margin. Confirmation URLs accumulate query parameters over years — order ID, email, order total, sometimes a hashed customer ID that isn't really hashed, occasionally a coupon code that was supposed to be single-use. Then marketing adds five tracking pixels to the same page because that's where conversions are counted. Every pixel request carries the full URL.
The parameters are often there for a bad reason: someone needed the order total for a conversion pixel and passing it in the URL was the fastest path. The right fix is to move that data into a JavaScript object rendered server-side, and pass it to the pixel as a parameter of the pixel call rather than smuggling it through the page URL. But that's a refactor. The header change is the tourniquet you apply first.
Password reset and magic links
A reset URL contains a token that is the credential. If that page loads any third-party resource — a font from a CDN, a chat widget, an analytics beacon — the token goes with it. The third party is now one curl away from taking over the account, and the token is sitting in their access logs where it will be retained for however long their retention policy says.
This one deserves no-referrer specifically, applied to that route, in addition to the sensible site-wide default. Belt and braces. And the token should be single-use and short-lived so a log entry ages into uselessness quickly.
Internal search results
/search?q=maternity+support+belt tells a third party something about the customer that the customer did not agree to share. Search queries are among the most revealing data a retailer holds — health, finances, gifts, things people are embarrassed about. Pharmacy and intimate-apparel retailers should treat their search query strings as sensitive data, full stop.
Account and address pages
Anything under /account/ that carries an identifier: /account/orders/100047821, /account/addresses/edit/9931. These are path segments rather than query parameters, which people often forget are equally exposed. Origin-only policies strip the path too, which is the point.
Cart and session tokens in URLs
Some platforms — older Magento installs and a few headless setups I've inherited — put a cart or quote identifier in the URL for share-a-cart or guest-recovery features. That identifier frequently grants read access to the cart contents, sometimes to the customer's stored address. Treat it like a credential.
Preview and staging URLs
Less about customer privacy, more about commercial confidentiality. A signed preview URL for next season's collection, leaked in a referrer to an ad network, is how competitors and journalists find your unannounced products. This happens more than anyone admits.
5. Picking Your Default
My recommendation for essentially every commercial storefront:
Referrer-Policy: strict-origin-when-cross-origin
The reasoning is straightforward. Internally — your own origin — you keep the full URL, so your funnel analysis, internal search reporting, and your own server logs remain complete. Externally, third parties learn only that traffic came from your domain, which is exactly the amount of information an attribution system needs and no more. And nothing at all is sent when a request downgrades to plain HTTP, closing the network-observer hole.
It also happens to match what Chrome, Firefox, and Safari already do by default. That is a feature, not a reason to skip it. Setting the header explicitly means your policy is a decision recorded in your configuration rather than an accident of browser defaults that could shift under you, and it means older or unusual clients get the same treatment. I have also lost an afternoon to a CDN that injected its own permissive policy when the origin sent none; an explicit header would have prevented it.
Where I deviate:
no-referreron password reset, magic-link login, unsubscribe confirmation, and any signed-URL route.same-originon logged-in account areas for retailers in regulated categories — pharmacy, adult, financial services — where I'd rather send nothing outward than send an origin that implies a customer relationship.strict-origin(note: no "when-cross-origin") on a handful of sites where even internal full-URL referrers were feeding a poorly-governed internal log pipeline. Rare, and it costs you funnel data.
What I do not do is set unsafe-url site-wide to keep one affiliate partner happy. If a partner genuinely needs the full URL, that's a per-link override, not a site-wide posture. See the section on per-element control.
6. Implementation Across the Stacks
Set it at the edge if you can. One place, applies to everything, survives application deploys. Set it in the application only when you need per-route variation the edge can't express cleanly.
Nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Routes where the URL is itself a credential
location ^~ /customer/account/createPassword {
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
The always flag matters more than it looks. Without it, Nginx omits add_header directives on error responses — 4xx and 5xx — and your 404 page is a real page that real browsers render and that really loads third-party resources. Add always everywhere.
The other Nginx trap, and this one has bitten me twice: add_header does not inherit into a nested block that declares its own add_header. If a location block sets any header, it drops all inherited ones from the parent. That's why the block above repeats X-Content-Type-Options and HSTS. Forget that and you'll silently strip your security headers on exactly the route that needed them most.
Apache
<IfModule mod_headers.c>
Header always set Referrer-Policy "strict-origin-when-cross-origin"
<LocationMatch "^/customer/account/(createPassword|resetPassword)">
Header always set Referrer-Policy "no-referrer"
</LocationMatch>
</IfModule>
Apache's Header set replaces rather than appends, and inheritance works the way you'd expect, so this is less error-prone than the Nginx equivalent. Use set, not add — add will happily send the header twice, and browsers handle duplicate Referrer-Policy values by taking the last valid one, which turns your config into a coin flip.
Cloudflare Workers
export default {
async fetch(request, env, ctx) {
const response = await fetch(request);
const headers = new Headers(response.headers);
const url = new URL(request.url);
const sensitive = /^\/(account|customer\/account|reset-password|unsubscribe)/;
headers.set(
'Referrer-Policy',
sensitive.test(url.pathname) ? 'no-referrer' : 'strict-origin-when-cross-origin'
);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
};
Doing it at the edge worker has one real advantage over Transform Rules in the dashboard: it lives in version control and goes through code review. Dashboard rules are invisible to your repo and someone will change one at 11pm during an incident and forget to tell you.
Magento 2
Magento ships a Magento_Csp module but does not manage Referrer-Policy for you. The clean insertion point is a plugin on the response, or simply the web server — which is what I'd normally recommend. If you need it in the application because your route logic is complicated:
<?php
namespace Modracx\Security\Plugin;
use Magento\Framework\App\Response\Http as HttpResponse;
class ReferrerPolicy
{
private const SENSITIVE_ROUTES = [
'customer/account/createpassword',
'customer/account/forgotpasswordpost',
];
public function __construct(
private \Magento\Framework\App\Request\Http $request
) {
}
public function beforeSendResponse(HttpResponse $response): void
{
$path = strtolower(trim($this->request->getPathInfo(), '/'));
$policy = in_array($path, self::SENSITIVE_ROUTES, true)
? 'no-referrer'
: 'strict-origin-when-cross-origin';
$response->setHeader('Referrer-Policy', $policy, true);
}
}
The third argument to setHeader is the replace flag. Pass true. Without it you can end up with two policy headers when a third-party module sets its own, and you're back to the coin flip.
Shopify
You do not control response headers on a Shopify storefront. Shopify sets Referrer-Policy: strict-origin-when-cross-origin itself, which is the value you'd have chosen anyway, so on a standard theme this problem is already solved for you.
What you still control, and should audit, is the shape of your URLs. Shopify's order status page carries identifiers, and any app you install can add parameters to it. Your work on Shopify is parameter hygiene and app review, not header configuration. Check what your apps append to checkout and thank-you URLs, and check it again after every app install.
Next.js and other Node front ends
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }
]
},
{
source: '/account/:path*',
headers: [{ key: 'Referrer-Policy', value: 'no-referrer' }]
}
];
}
};
Next.js applies the first matching rule set per path, and both blocks can match /account/settings. Order matters and the behaviour has changed between major versions. Verify with an actual request rather than trusting the config to do what you meant.
The meta tag, and why it's a fallback
<meta name="referrer" content="strict-origin-when-cross-origin">
This works, and it is the only option if you genuinely cannot set response headers — some hosted platforms, some CMS setups. But it has a real weakness: it only takes effect once the parser reaches it. Any resource requested before that point uses the previous policy. Put it as early in <head> as you can, and prefer the header when you have the choice. Where both exist, the meta tag wins for the document, which makes it a useful override on one specific page of an otherwise header-governed site.
7. Per-Element and Per-Request Control
Site-wide policy is the floor, not the ceiling. When a specific partner needs more, or a specific link needs less, you override at the point of use. This is the mechanism that lets you keep a strict default without breaking the one integration that genuinely requires a full URL.
On links, images, iframes, scripts, and forms, use the referrerpolicy attribute:
<!-- This partner's attribution genuinely needs the landing path -->
<a href="https://partner.example.net/track" referrerpolicy="strict-origin-when-cross-origin">
Compare prices
</a>
<!-- Send nothing at all to an untrusted destination -->
<a href="https://forum.example.org/thread/9912" rel="noreferrer noopener">
Discussion thread
</a>
<!-- A tracking pixel that must not learn the page URL -->
<img src="https://pixel.example.net/p.gif" alt="" referrerpolicy="no-referrer" width="1" height="1">
rel="noreferrer" is the older mechanism and it does two things at once: suppresses the referrer and severs window.opener. That second effect is why it's bundled with noopener in most advice. If you only want the referrer behaviour, referrerpolicy="no-referrer" is the precise tool. If you want both, rel="noreferrer" is fine and shorter.
For scripted requests, pass it to fetch:
// Analytics beacon that should not carry the checkout URL
await fetch('https://metrics.example.net/collect', {
method: 'POST',
referrerPolicy: 'no-referrer',
body: JSON.stringify(payload),
keepalive: true
});
// Internal API call where the full referrer is useful for debugging
await fetch('/api/cart/refresh', {
referrerPolicy: 'same-origin'
});
Note the casing difference, which will cost you ten minutes at some point: the HTML attribute is lowercase referrerpolicy, the JavaScript property is camelCase referrerPolicy, and the HTTP header is Referrer-Policy.
You can also set an explicit referrer per request with the referrer option, though the browser will refuse values outside your origin. It's occasionally useful for making internal calls report a canonical path rather than a parameterised one.
8. What Breaks, and Who Will Tell You About It
This is the section that determines whether your change survives contact with the business. Every item here has cost me a conversation.
Affiliate attribution
The loudest failure. Some affiliate networks — particularly older ones and some regional networks — match conversions by parsing the referring URL rather than by cookie or click ID. Tighten the policy and their matching rate falls, publishers stop getting paid, and your affiliate manager escalates within days.
The correct fix is to move that partner onto click-ID-based tracking, which every serious network supports and which is more accurate anyway. The interim fix is a per-link override on the specific outbound links to that network. Do not fix it by loosening the site-wide policy.
Analytics referral reporting
Here's the nuance people get wrong: tightening your policy does not affect how you see traffic arriving at your site. Your inbound referrals are governed by the policy of the sites linking to you, which you don't control. What your policy affects is what you reveal when sending traffic outward, and how much URL detail your own analytics receives on cross-origin beacons.
In practice, with GA4 or a server-side collector, strict-origin-when-cross-origin changes almost nothing, because the page URL is passed explicitly in the measurement payload rather than inferred from the referrer. Where it does bite is homegrown analytics that parse document.referrer, and any tool doing cross-subdomain journey stitching without a shared identifier.
CDN hotlink protection
Referrer-based hotlink protection breaks when referrers stop carrying the path, and sometimes when they stop being sent at all. If your image CDN is configured to allow requests only when the referrer matches your domain, an origin-only policy still satisfies it — the origin is present. A no-referrer route will not, and your images will 403 on exactly the page where you tightened the policy hardest.
I ran into this on a password reset page: the policy was correct, and the company logo at the top of the page vanished. Took longer to diagnose than I'd like to admit. Signed URLs or token-based protection are the real answer; referrer checks were never security.
Payment and fraud tooling
Some fraud scoring vendors use referrer as one weak signal among dozens. Removing it typically shifts scores imperceptibly. Some 3-D Secure flows and older hosted payment pages, however, validate the referrer on the return leg. Test the full payment path in a sandbox before shipping, including the failure and retry branches, which is where the odd redirect chains live.
Embedded content and iframes
Third-party embeds — video players, review widgets, store locators — occasionally use the referrer to determine which customer account they belong to. Vimeo's domain-restriction feature is the classic example. Origin-only referrers keep these working; no-referrer does not. This is another argument for reserving no-referrer for specific routes rather than applying it broadly.
Internal tools and support workflows
Worth checking: does your support team have a dashboard that shows the page a user was on when they opened a chat? Does your error tracker record referrers to reconstruct journeys? Neither is a reason to keep leaking customer data, but both are reasons to warn the people who use them before their tooling quietly degrades.
9. How Attribution Actually Works Now
Half the resistance you'll meet comes from people who believe the referrer is load-bearing for measurement. On most modern stacks it stopped being load-bearing years ago, and understanding why turns a political argument into a technical one you can win.
Three mechanisms carry attribution today, in descending order of reliability.
Click identifiers. Google appends gclid (or wbraid and gbraid on iOS), Meta appends fbclid, Microsoft appends msclkid, and most affiliate networks append a click ID of their own. These land in your URL as query parameters on arrival, get captured by your tag or your server, and are stored against the session. They survive referrer stripping entirely, because they are not referrers — they are parameters on the inbound URL. Your policy governs outbound requests and has no effect on them whatsoever.
This is the single most useful sentence in the whole discussion: tightening your Referrer-Policy cannot break inbound click-ID attribution. If your paid channels are measured by click ID, and they almost certainly are, they are untouched.
First-party session storage. Once the click ID is captured, a well-built stack writes it to a first-party cookie and reads it again at conversion. The referrer never enters the chain. Server-side tagging strengthens this further, because the conversion event is assembled on your server from data you already hold rather than reconstructed in the browser from whatever headers happened to survive.
The referrer itself. Genuinely useful in exactly one place: organic and direct traffic, where there is no click ID to work with. Knowing a visitor arrived from a specific blog or a specific search engine has real value. And here is the thing people miss — that is governed by the referring site's policy, not yours. You could set no-referrer tomorrow and your inbound organic referral reporting would not change by a single session.
So when the marketing lead asks what this does to attribution, the accurate answer has three parts. Inbound paid attribution: no change, it runs on click IDs. Inbound organic referral reporting: no change, it depends on other sites' policies. Outbound reporting to partners who parse your URLs: reduced, deliberately, and those partners should move to click IDs like everyone else.
The exception worth naming honestly is legacy affiliate networks in certain regions, and some price-comparison engines, which still do referrer-string matching. They exist. They are a shrinking minority, and the per-link override described below handles them without compromising the rest of the site.
10. Migrating Without Losing the Argument
The failure mode on this project is not technical. It's that you ship a correct change, an attribution number drops, someone blames the change, and it gets reverted along with the actual privacy fix. Measure first so you can answer that.
Step one: find out what you're currently sending
Check the live header before you assume anything:
curl -sI https://shop.example.com/ | grep -i referrer-policy
curl -sI https://shop.example.com/checkout/success | grep -i referrer-policy
# And the one people forget — error pages
curl -sI https://shop.example.com/definitely-not-a-real-page | grep -i referrer-policy
Step two: inventory the URLs that carry secrets
Pull a month of URLs from your access logs or analytics and look for parameters that should not exist. A crude but effective sweep:
awk '{print $7}' /var/log/nginx/access.log \
| grep -oP '\?.*' \
| tr '&' '\n' \
| cut -d= -f1 \
| sort | uniq -c | sort -rn \
| head -60
Read that list with suspicion. Anything resembling email, token, order, customer, phone, postcode, total, or hash deserves an explanation. Most will turn out to be a shortcut someone took years ago that nothing depends on any more.
Step three: baseline the metrics that will be questioned
Before you change anything, write down current values for affiliate-attributed revenue, referral-channel sessions, and any partner-reported conversion counts. Two weeks of daily figures if you can get them. When someone claims the header change cost them 8% of conversions, you want a chart, not a memory.
Step four: roll out narrow to wide
Apply no-referrer to the genuinely sensitive routes first. That's the highest-value, lowest-risk change: those pages have almost no attribution role. Then move the site-wide default. Then, separately and later, do the real work of removing personal data from the URLs entirely — because the header protects you from third parties, but the parameters are still sitting in your own access logs, your CDN logs, your WAF logs, and every browser history on every shared family computer.
That last point is the one worth repeating to stakeholders. Referrer-Policy is a mitigation. Not putting personal data in URLs is the fix.
11. A Worked Migration, With Numbers
A mid-size fashion retailer on Magento 2.4.6, roughly 40,000 orders a year, about 18% of revenue attributed to affiliates. They came to me for a PCI-adjacent security review and this came out of it.
What we found. The header was no-referrer-when-downgrade, set in a Varnish VCL snippet in 2017 by someone who had since left. The success page URL carried five parameters: order, email, total, currency, and a sig that turned out to be an MD5 of the order ID and a hardcoded salt. Eleven third-party resources loaded on that page — two analytics, one heatmap, one chat widget, one review platform, three ad pixels, two fonts, and a legacy tag nobody could identify. All eleven received the full URL on every order.
The sig parameter was the interesting one. It was used to authorise a "view your order" link, meaning any of those eleven vendors could reconstruct order lookups for every customer. That reframed the conversation from "privacy hygiene" to "third parties can read our customers' orders," which is a sentence that gets budget.
What we measured first. Fourteen days of daily affiliate-attributed revenue, referral sessions by source, and the network's own reported conversion count. The two conversion counts already disagreed by about 4%, which is normal and which was useful later, because it meant nobody could claim the numbers had ever been exact.
What we shipped, in order. Week one: no-referrer on the password reset and account routes only. No metric moved, because those pages had no attribution role. This was deliberate — a first change that visibly does nothing buys trust for the second.
Week two: site-wide strict-origin-when-cross-origin, replacing the Varnish snippet with a committed Nginx config. Affiliate-attributed revenue fell 2.1% against the baseline over the following fortnight. One of their four networks accounted for essentially all of it; the other three were unmoved because they were on click IDs.
Week three: per-link referrerpolicy override on outbound links to the affected network, plus an opened ticket with that network asking for click-ID support. Attribution recovered to within 0.4% of baseline, inside the normal week-to-week noise.
Weeks four through nine: the actual fix. The success page was refactored to take an opaque, single-use, fifteen-minute token instead of five parameters, with order details rendered server-side into a JavaScript object for the pixels to read. Two of the eleven third-party tags were removed during the audit because nobody could name an owner, which incidentally took about 240ms off the page's load time.
What it cost. Around three days of engineering across six weeks, most of it in the refactor rather than the header. The header itself was under an hour including testing.
What I'd do differently. I'd have run the parameter sweep before the security review rather than during it, because the sig finding is what unlocked the budget and we found it by accident on day three. And I'd have warned their support team earlier — their helpdesk tool showed the last page a customer visited, that display got less specific, and I heard about it from a support manager rather than proactively telling her. Small thing, entirely avoidable, and the kind of omission that makes people distrust the next change you propose.
12. Verifying It Actually Works
Configuration that isn't verified is a guess. Three levels of checking, cheapest first.
The header itself
curl -sI https://shop.example.com/account/orders | grep -i referrer-policy
# Referrer-Policy: no-referrer
Check a representative URL from every rule you wrote, plus one that should fall through to the default, plus a 404. If you use a CDN, run the check against the edge and against the origin — they can disagree, and the edge is what customers get.
Actual browser behaviour
The header is a request to the browser, not a guarantee. Confirm what's actually being sent. Open DevTools, Network tab, click a request, look at the request headers. Or drive it from the page:
// Paste in the console on the page under test
console.log('policy:', document.referrerPolicy || '(unset — using browser default)');
console.log('this page was reached from:', document.referrer || '(nothing)');
Then click an outbound link to a request bin you control and read what arrived. A free request-inspection service works; so does a two-line Node server on a spare host. Trust the received header over the configured one.
Continuous checking
Headers regress. A CDN configuration change, a platform upgrade, a well-meaning module — any of them can drop your policy without a deploy touching your code. Put it in a smoke test that runs after every release:
#!/usr/bin/env bash
set -euo pipefail
check() {
local url="$1" want="$2"
local got
got=$(curl -sI "$url" | tr -d '\r' | awk -F': ' 'tolower($1)=="referrer-policy"{print $2}')
if [ "$got" != "$want" ]; then
echo "FAIL $url — want '$want', got '${got:-nothing}'" >&2
return 1
fi
echo "ok $url — $want"
}
check https://shop.example.com/ "strict-origin-when-cross-origin"
check https://shop.example.com/account/orders "no-referrer"
check https://shop.example.com/no-such-page "strict-origin-when-cross-origin"
Free external scanners will also grade your headers and are worth running quarterly, but they check the homepage. Your leak is on the checkout success page. Write your own check against the URLs that matter.
13. Browser Behaviour and the Remaining Quirks
Support is effectively universal — every browser you care about has honoured Referrer-Policy for years, and the browser defaults are now stricter than most sites' explicit configuration. A handful of behaviours are still worth knowing.
Defaults have converged, but they are defaults. Chrome moved to strict-origin-when-cross-origin in version 85, Firefox followed, and Safari has long applied aggressive referrer trimming of its own, downgrading cross-site referrers to the origin and in some tracking-prevention cases stripping them further. Do not build anything that depends on receiving a full cross-site referrer. It is not coming.
Safari trims more than it advertises. Intelligent Tracking Prevention applies its own rules on top of your policy, and they are not fully documented. If your measurements disagree between Safari and Chrome and you cannot explain the gap, this is usually why. Do not chase it; design so it does not matter.
Invalid values fall back silently. Misspell the policy — strict-origin-when-cross-orgin, which I have shipped — and the browser ignores the header entirely and applies its default. No console warning in most browsers, no error, nothing. Your site is then protected by the browser default rather than by you, which happens to be fine in this case and would not be if you were trying to set something stricter. This is exactly why the smoke test compares against an expected string rather than merely checking the header exists.
Multiple values are allowed, and useful. The header accepts a comma-separated list, and browsers take the last value they understand. That gives you a graceful upgrade path for any newer value: Referrer-Policy: no-referrer, strict-origin-when-cross-origin means an old client that only knows no-referrer uses it, and a current one uses the second. In practice there is no value new enough to need this today, but it is worth recognising when you see it in someone else's config rather than assuming it's a mistake.
Redirects use the policy of the redirecting page. If a customer clicks a link on your checkout page that redirects through your own /out?url= handler before landing on a partner, the referrer the partner sees is governed by the policy on the redirector, not the original page. Link-cloaking handlers are therefore a decent place to apply a deliberately different policy — and a place where I have twice found a permissive one left behind.
14. How It Interacts With Everything Else
Referrer-Policy does not live alone, and a few interactions surprise people.
Content Security Policy. There was once a referrer directive inside CSP. It was removed from the specification and you should not use it; browsers that still honour it are inconsistent. Keep the concerns separate: CSP governs what may load, Referrer-Policy governs what those loads reveal.
SameSite cookies. Unrelated mechanisms that solve adjacent problems. SameSite stops your cookies riding along on cross-site requests; Referrer-Policy stops your URLs doing the same. Both belong in the same hardening pass, and neither substitutes for the other.
Cross-Origin-Opener-Policy. Setting rel="noreferrer" on links severs window.opener as a side effect, which overlaps with what COOP does at the document level. If you've deployed COOP, you can rely on referrerpolicy alone and skip noreferrer, keeping intent clearer.
SEO. Since this comes up in every kickoff: Referrer-Policy has no direct effect on rankings. Googlebot does not use referrers to discover or evaluate pages, and no policy value will help or hurt your positions. What it can affect is your reporting — if a colleague insists rankings dropped because of this header, the change was coincidental. The indirect and genuine SEO benefit is that a clean security header profile is one fewer thing for an audit to flag, and that the discipline of removing parameters from URLs tends to clean up duplicate-content problems at the same time.
Consent management. If you operate under GDPR, note that referrer leakage happens before any consent decision if the leaking resource loads early. A consent banner that blocks scripts until acceptance is doing a lot of the same work, and the two controls together are far stronger than either alone. Auditors increasingly ask about this specific combination.
15. Questions I Get Asked
"Will this hurt our Google rankings?" No. Googlebot does not send or consume referrers in any way that affects crawling, indexing, or ranking. If positions move the week you ship this, look elsewhere — and be a little suspicious of anyone who tells you otherwise.
"Can we just use no-referrer everywhere and be done?" You can, and some privacy-focused sites do. On a commercial storefront you'll lose internal funnel analysis, break domain-restricted embeds, break referrer-based hotlink protection, and hand your affiliate manager a genuine problem. strict-origin-when-cross-origin gets you most of the privacy benefit with almost none of the breakage. Spend the difference elsewhere.
"Our security scanner says the header is missing but I set it." Three usual causes: the scanner checked a URL that hit a route with its own add_header block and lost inheritance; the CDN stripped or replaced it; or the header is only on 200 responses because always was omitted in Nginx. Check the edge and the origin separately, and check a 404.
"Does this protect against XSS or CSRF?" No. Different problems entirely. It reduces what leaks through a legitimate request; it does nothing about injected scripts or forged requests. Referrer checking as a CSRF defence is weak on its own — the header is forgeable outside a browser and absent under strict policies — so if you find code relying on it, that's a finding of its own.
"We're on Shopify, do we need to do anything?" Not for the header. Do audit what your apps append to checkout and thank-you page URLs, and re-audit after each install. Parameter hygiene is the part of this you still own.
"The marketing team wants unsafe-url for one campaign." Give them a per-link referrerpolicy override on the specific outbound links, scoped to that campaign, with an expiry date in the ticket. Never the site-wide default. This is the request I get most often and the one where holding the line matters most, because a temporary site-wide loosening is permanent in practice.
16. Keeping It Fixed
Every hardening project has a half-life. Ways to extend this one's:
Put the header in infrastructure-as-code, not in a dashboard. A Terraform resource or a committed Nginx snippet gets reviewed; a Cloudflare UI toggle does not.
Add the smoke test above to your deploy pipeline and let it fail the build. A test nobody runs is a comment.
Write a two-line rule into whatever passes for your engineering standards: personal data does not go in URLs. Then add a lint check on route definitions if your framework makes it possible. The next person who needs an order total on the success page will reach for the query string unless something stops them.
Re-run the parameter sweep from the migration section twice a year. New parameters appear constantly, usually from marketing tooling and app installs, and nobody announces them.
17. What This Is Really About
The header is easy. I could have written this in four hundred words: set strict-origin-when-cross-origin, use no-referrer on credential-bearing routes, override per element where a partner truly needs more.
The part that takes judgment is knowing that the leak in the screenshot at the top was not caused by a missing header. It was caused by someone, three years earlier, needing an order total on a confirmation page and taking the fastest available route. The header change stops third parties reading it. It does nothing about the customer's email address sitting in your access logs for the ninety days your retention policy allows, or in the browser history of a shared laptop, or in the URL that customer pastes into a support chat when something goes wrong.
Fix the header this week, because it's cheap and it closes the widest exposure. Then open the longer conversation about why the data was in the URL at all. That second one is where the actual privacy improvement lives, and it's the one that never makes it onto a roadmap unless someone with a screenshot insists.