The HTTP Referer request header can tell a destination which page initiated a request. Its historical spelling differs from Referrer-Policy, the policy that controls how much source information the browser supplies. Depending on that policy and the destination, the value may include the source path and query, contain only the source origin, or be omitted. Sending the full URL to every external destination is not the modern default.
The usual current default is strict-origin-when-cross-origin. It sends path and query information for same-origin requests, only the origin across origins at an acceptable security level, and no referrer on a security downgrade such as HTTPS to HTTP. Explicitly selecting a policy still makes the intended behavior reviewable. Origin-only disclosure is smaller than a full URL, but the hostname itself can remain meaningful information.
Distinguish the policy choices
no-referrer omits the header. same-origin retains the fuller value only for same-origin destinations. unsafe-url can disclose origin, path, and query more broadly, including where stricter policies would suppress them. URL fragments are removed from the referrer calculation, so a fragment visible in the address bar should not appear in the received Referer header. Do not confuse that omission with a promise that fragments are secret from page scripts or browser state.
Set the document policy through the response header, or use HTML’s <meta name="referrer" content="..."> mechanism when appropriate. An invented http-equiv="Referrer-Policy" tag is not the equivalent syntax. Some elements and fetch requests also support a per-request referrer policy. These possibilities mean that the document header alone may not explain every observed request; inspect the initiating context and any explicit request-level choice.
When it breaks
An application can depend on receiving a full Referer path and then fail when a stricter policy or browser privacy behavior removes it. Referrer data is not a reliable authentication credential or a substitute for explicit application parameters. Likewise, changing this policy cannot grant CORS access to a response or remove an access token already placed in the destination URL. It controls one piece of request metadata, not every information flow.
Another mistake is assuming cross-origin and cross-site are interchangeable. Different ports on the same host are different origins, which is enough to change the policy result in this example. Same-origin requests can still reveal sensitive path or query information to another endpoint on that origin. Review what each receiver genuinely needs, and inspect redirect chains or resource-specific policies when the observed value differs from the document’s apparent default.
Check it quickly
Save this as referrer-policy.py, run python3 referrer-policy.py --serve, and open the printed page URL with ?policy=strict-origin-when-cross-origin&demo=visible#fragment-not-sent. Python 3.14.4 and Chrome 152 were used. Two temporary loopback servers provide a page and a cross-origin receiver. The page fetches both a same-origin and a cross-origin endpoint, which return the Referer values they actually received. Control-C closes both servers.
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Event, Thread
from urllib.parse import parse_qs, urlsplit
from urllib.request import urlopen
policies = {"strict-origin-when-cross-origin", "no-referrer", "same-origin", "unsafe-url"}
class Receiver(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
body = json.dumps({"referer": self.headers.get("Referer")}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.send_header("Access-Control-Allow-Origin", app_origin)
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
class App(Receiver):
def do_GET(self):
url = urlsplit(self.path)
if url.path == "/collect":
return super().do_GET()
policy = parse_qs(url.query).get("policy", ["strict-origin-when-cross-origin"])[0]
assert policy in policies
body = ("""<!doctype html><meta charset="utf-8"><title>Referrer policy lab</title>
<h1>What did the receiver see?</h1><pre id="result">Running...</pre><script>
const receiver=""" + json.dumps(receiver_origin) + """;
async function run() {
const same=await (await fetch('/collect')).json();
const cross=await (await fetch(receiver+'/collect')).json();
window.labResult={page:location.href,same:same.referer,cross:cross.referer};
document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2);
}
run().catch(error=>{window.labError=String(error);});
</script>""").encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Referrer-Policy", policy)
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
receiver = HTTPServer(("127.0.0.1", 0), Receiver)
app = HTTPServer(("127.0.0.1", 0), App)
receiver_origin = f"http://127.0.0.1:{receiver.server_port}"
app_origin = f"http://127.0.0.1:{app.server_port}"
threads = [Thread(target=server.serve_forever, daemon=True) for server in (receiver, app)]
for thread in threads:
thread.start()
try:
for policy in sorted(policies):
with urlopen(app_origin + "/page?policy=" + policy + "&demo=visible", timeout=5) as response:
assert response.headers["Referrer-Policy"] == policy
response.read()
print(json.dumps({"app_url": app_origin + "/page", "wire_policies": sorted(policies)}), flush=True)
if "--serve" in sys.argv:
Event().wait()
except KeyboardInterrupt:
pass
finally:
for server in (receiver, app):
server.shutdown()
server.server_close()
for thread in threads:
thread.join(timeout=5)
Under the strict default policy, the same-origin result includes /page, the policy parameter, and demo=visible, while the cross-origin result contains only the page origin and trailing slash. Neither contains the fragment. Change the policy parameter to no-referrer: both results become null. With same-origin, only the same-origin result remains. With unsafe-url, both receivers obtain the source path and query. All four cases were verified in the browser.
The Python-only run checks policy headers, not browser referrer calculation. This fixture uses equal-security HTTP loopback origins; it does not exercise HTTPS downgrades, redirects, external stylesheets, or every browser privacy setting. Those cases need checks in their actual contexts. After selecting a production policy, confirm what the receiving endpoint sees rather than inferring it from the address bar, and keep sensitive values out of URLs when their persistence elsewhere would still be a problem.
Sources
- MDN Referrer-Policy, policy values, the current default, and HTML integration, checked 2026-09-13.
- W3C Referrer Policy, referrer calculation, origin boundaries, and URL filtering, checked 2026-09-13.