HTTP 的 Referer 请求头可以告诉接收方,这次请求由哪个页面发起。它保留了历史拼写,与控制来源信息范围的 Referrer-Policy 并不完全同名。根据策略和目标地址,值可能包含源页面的路径与查询参数,也可能只剩来源部分,或者完全省略。现代浏览器的默认行为并不是向每一个外部目标发送完整地址,不能沿用这种错误前提解释所有来源信息泄露。

当前通常采用的默认策略是严格的跨源仅来源策略:同源请求可以发送路径和查询信息,安全级别合适的跨源请求只发送来源,遇到从 HTTPS 到 HTTP 等安全降级则不发送引用来源。主动设置策略,仍有助于让团队审阅明确的预期行为。只暴露来源虽然少于完整地址,但主机名称本身仍然可能带有业务含义,不能未经判断就把来源级信息一概称为无敏感性的统计数据。

区分几种策略的具体选择

不发送引用来源的策略会省略整个请求头,同源策略只对同源目标保留较完整的值,而不安全地址策略会更广泛地披露来源、路径和查询参数,包括一些严格策略本来会抑制的场景。地址片段会从引用来源计算中移除,因此地址栏里的片段不应出现在接收方的 Referer 中。但这并不表示片段对页面脚本或浏览器状态保密,它只是没有通过这个请求头传送。

文档策略可以通过响应头设置,在合适场景下也可以使用名称为 referrer 的 HTML meta 元素。不能把自造的 http-equiv=“Referrer-Policy” 标签当成同等语法。部分元素以及 fetch 请求还支持单次请求自己的引用来源策略,因此单看文档响应头不一定能解释每一个实际请求。观察到不同结果时,应检查请求由什么上下文发起,以及它是否明确指定了自己的策略。

坏掉的样子

应用如果依赖收到完整 Referer 路径,可能在策略收紧或浏览器隐私行为减少信息后突然失效。引用来源并不是可靠的认证凭据,也不能代替明确的应用参数。同样,修改这项策略不会授予读取跨源响应的 CORS 权限,也不会删除已经写入目标地址的访问令牌。它只控制请求中的一种元数据,不能被当成同时处理全部信息流动的开关。

另一个错误是把跨源与跨站混为一谈。同一主机上的不同端口已经属于不同来源,足以改变本例的策略结果。同源请求也仍然可能把敏感路径或查询信息交给该来源上的另一个接口。需要核对每个接收者实际需要什么;当观察到的值与文档看起来采用的默认值不一致时,还要检查重定向链及资源自己的策略,不能只依据域名相似就认定浏览器表现异常。

快速验证

保存脚本为 referrer-policy.py,执行 python3 referrer-policy.py –serve。在打印出的页面地址后追加 ?policy=strict-origin-when-cross-origin&demo=visible#fragment-not-sent 并打开。本次使用 Python 3.14.4 与 Chrome 152,两个临时回环服务分别提供页面和跨源接收端。页面实际请求同源接口与跨源接口,由接口返回自己收到的 Referer 值,结束后按控制键加 C 关闭两个服务。

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)

使用严格默认策略时,同源结果应包含页面路径、策略参数和演示查询值,跨源结果只包含页面来源及结尾斜杠,两者都没有片段。将策略参数改成 no-referrer 后,两个结果都为空;改成 same-origin 后,只保留同源结果;改成 unsafe-url 后,两边都能收到源路径与查询信息。这四种情况均经过实际浏览器验证,不是通过 Python 手工组装请求头来代替浏览器的计算行为。

不带服务参数运行 Python 时,只检查策略响应头,不会验证浏览器怎样计算引用来源。本例使用安全级别相同的本机 HTTP 来源,没有执行 HTTPS 降级、重定向、外部样式表或全部浏览器隐私设置的检查,这些情况需要在真实上下文中另行验证。生产策略选定后,应以接收端实际得到的信息为准,而不是从地址栏猜测请求头;如果敏感值在其他位置继续留存也构成问题,就仍然不应该把它放进地址中。

延伸阅读

来源

  • MDN Referrer-Policy,策略值、当前默认行为与 HTML 集成方式,已核验 2026-09-13。
  • W3C Referrer Policy,引用来源计算、来源边界与地址过滤,已核验 2026-09-13。