前言
本文将继续叙述常见的CSRF类型,并且本次将重点放在SameSite 属性,因为现代的Web安全已经全面来推荐它,而它的作用也十分明显,就是限制当出现跨站请求时,到底是否要不要带Cookie的问题。
什么是 SameSite?它要解决什么问题?
SameSite 是 HTTP 响应头 Set-Cookie 中的一个属性。它赋予了服务器一种能力:告诉浏览器,在跨站(Cross-Site)发起请求时,到底要不要带上这个 Cookie。
它面对的目标与解决的问题
在这个属性被全面推广前,浏览器的默认行为时"无论任何来源的请求,只要域名匹配就会带上的这个域名的所有Cookie"。
- 带来的灾难: :这导致了CSRF的泛滥,同时也会让广告商也可以肆无忌惮的利用Cookie追逐用户。
- SameSite的使命: 从浏览器底层切断与第三方的Cookie滥用,作为防守CSRF的底层防线。
SameSite的三种模式
- Strict: 完全禁止第三方Cookie,只有用户网页URL与请求目标URL完全同站时,才会带有Cookie。哪怕你是从别人发给你的链接点进来的(GET 请求),第一次跳转过去时也不会带 Cookie。这几乎彻底绝杀了 CSRF,但用户体验极差(比如从外部链接点进 GitHub,会发现自己处于未登录状态,刷新一下才出来)。
- None: 完全放开,无论如何都会带上Cookie,但是现代框架下,若要设置它,则必须带有Secure属性(即只能在 HTTPS 下传输) 。
- Lax: 折中方案,如果不指定SameSite属性的话,默认就是这个,它会拦截跨站请求,但是会放开常规的顶级导航。
Lax机制
Lax的核心逻辑是,放行常规顶级导航,具体是在跨站发起请求时:
- 放行(带有Cookie):引起顶级导航(地址栏 URL 改变)且为"安全 HTTP 方法"(如 GET)的请求。比如点击 < a > 标签跳转、使用 window.location.href 重定向、或者通过 < link rel="prerender" >。
- 拦截(不带 Cookie): 所有的跨站 POST、PUT、DELETE 请求(比如通过 < form method="POST"> 提交表单),以及所有的异步 AJAX 请求(XHR/Fetch)和 < img>、< iframe> 等内联资源的加载。
顶级导航
在简单说一下顶级导航,什么是顶级导航?
它的定义非常直观:它会导致浏览器地址栏里的 URL 发生改变,整个页面的主文档被替换。
当你触发顶级导航时,浏览器知道这是用户主动离开页面,并且由于它是显式跳转,Lax则会允许这个过程带有Cookie。
常见的顶级导航触发方式为:
- 普通点击:用户点击了 < a href="https://target.com">
- JavaScript显式重定向:执行了 window.location.href = "..." 或 window.location.assign(...) 或 window.location.replace(...)
- Meta标签刷新:HTML 头部包含了 < meta http-equiv="refresh" content="0;url=...">。
- 表单GET提交:用户点击了 < form method="GET" action="..."> 的提交按钮(地址栏参数改变,页面刷新)。
那么出去顶级导航外的,我们常见的例如< img src="...">、< link rel="stylesheet" href="...">以及使用 XMLHttpRequest (AJAX) 或 Fetch API 发起的网络请求。它们都属于子资源请求(Subresource Requests)或者嵌入式请求(Embedded Requests),它们的特点是浏览器的地址栏不会发生任何改变,页面主文档没有被替换。 浏览器只是在后台默默地拉取这些资源,用来渲染或丰富当前的页面,对于这些子资源请求面对Lax下是会被拦截的Cookie的,值得注意的是这其中一个特例:
- < iframe> 里的重定向:< iframe> 是一个非常特殊的元素。它在当前页面里嵌入了另一个完整的 HTML 文档。
如果你在一个恶意页面里嵌入了 < iframe src="https://target.com/api">它属于嵌套导航(Nested Navigation),不属于顶级导航,并且会被Lax的属性设置拦截。
导航类型总结
| 请求触发方式 | 请求类型 (Method) | 是否属于顶级导航 | SameSite=Lax 是否发送 Cookie? |
|---|---|---|---|
| < a > 标签点击 | GET | 属于 | 发送 |
| window.location.href | GET | 属于 | 发送 |
| < form method="GET"> | GET | 属于 | 发送 |
| < form method="POST"> | POST | 属于 | 拦截(POST方法会拦截) |
| < img src="..."> | GET | 不属于(子资源) | 拦截 |
| < script src="..."> | GET | 不属于(子资源) | 拦截 |
| < iframe> 加载 | GET | 不属于 (嵌套) | 拦截 |
| AJAX / Fetch API | GET/POST | 不属于(异步数据) | 拦截 |
文艺复兴,针对SameSite的具体执行过程
本文采用的Lab均来自portswigger。
Lab: SameSite Lax bypass via method override
这是一个通过:框架的 Method Override(方法重写)机制来绕过SameSite机制,具体的是对于跨站请求的表单采用GET方式提交时往往Lax会将其视为顶级导航从而放行Cookie,但是如果目标请求是POST方式,我们就可以借此实现绕过,因为存在Method Override方法重写机制,它在早期的 Web 架构中,很多老式浏览器或代理服务器不支持 PUT、DELETE 等 HTTP 方法,只支持 GET 和 POST。为了让 Web 应用能够遵循 RESTful 风格,很多后端框架(无论是传统的 PHP、Java,还是如今常用的 Go 和 Python Web 框架)都引入了一种"Workaround"(变通方案):
客户端可以通过一个特殊的参数(通常是 _method)来告诉后端:"虽然我发的是 GET/POST 请求,但请把我当做其他方法来处理。"
执行过程: 这个Lab期望我们可以通过CSRF来修改当前身份持有者的邮箱,而我们正常请求修改邮箱请求时,并不会发现SameSite参数,那么这表明浏览器采取了默认的SameSite:Lax

正常情况下,我们使用POST提交这个修改邮箱的请求,可以发现这里没有任何CSRF_Token操作,也没有SameStie的显式设置。

当我们主动使用GET方式提交,对方返回Error,表面这个请求不允许使用GET方式提交
javascript
<script>
document.location = "https://target.com/my-account/change-email?email=hacker@evil.com&_method=POST";
</script>
我们可以在漏洞利用服务器上置入这样一个恶意链接,它通过JS脚本通过重定向到目标网站后,请求修改邮箱的功能,并且使用_method=POST方式来设置Method Override(方法重写),将这个请求从GET方式悄悄改为POST的方式,绕过SameSite和修改邮箱请求的限制。

可以看到执行成功了,请求中携带了Cookie,并且Referer被标记为攻击浏览器了,直接骑脸了。
Lab: SameSite Strict bypass via client-side redirect
这是一个面对SameSite=Strict的Lab,它打破了很多人对设置这个属性就是绝对安全的滤镜。
当然Strict 模式就是 CSRF 的终极杀手:它规定只要请求是从外部站点发起的,无论你用什么姿势(点击链接、GET 表单),一律不带 Cookie。
所以碰到这样情况,那就只能请出内鬼了,如果设置了Strict模式,那么唯一可以做到绕过限制的就是,让内鬼自己请求,我们可以注意目标网站上是否存在一些恶意的注入位置,往往会是一些可以动态写入的重定向,它们的作用往往是记录你在站内的请求,从而在当前网页可以自动返回上一个页面,那么我们只需要制作一个恶意的路径穿越URL,就可以让这种重定向帮助我们执行恶意修改请求,由于这个请求时站内发送的,属于同站请求,可以完美绕过Strict模式。
执行过程: 进入这个Lab环境,我们会从任意的文章评论提交页面看到一个重定向JS:


当我们提交完评论后,会发现返回页面上存在一个自动执行的JS,它会延迟后自动重定向到之前到文章页面
javascript
<script>
document.location = "https://YOUR-LAB-ID.web-security-academy.net/post/comment/confirmation?postId=1/../../my-account/change-email?email=hacker@evil.com%26submit=1";
</script>
我们只需要将上述恶意JS提交到漏洞攻击服务器上去,模拟一个正常访客误触恶意链接,可以看到JS行为,它会注入到提交完评论的那个URL上去,将postId(文章ID)改为带有.../路径穿越的change-email,注意这里要使用URL转义&符号,将它顺利注入到后续的修改邮箱请求中,

这是过程,从攻击页开始/exploit,跳转到评论提交页面后,又经过站内的重定向,到了修改邮箱页面

最终提交了修改邮箱的请求,这里的Referer是站内地址,从而绕过限制,因为发起重定向的是站内的JS。
Lab: SameSite Lax bypass via cookie refresh
这是一个面对Lax机制的漏洞,它十分戏剧性性的通过利用Chrome 浏览器的一个"后门规则"(历史遗留的宽限期)。
Chrome 的"两分钟宽限期" (The 2-Minute Grace Period) :
现代浏览器(如 Chrome 80+)把没有显式设置 SameSite 属性的 Cookie 默认当作 SameSite=Lax 处理。这意味着跨站的 POST 请求(比如传统的 CSRF 表单提交)会被无情拦截。
但是,这个一刀切的策略引发了一个巨大的灾难:破坏了全网无数的单点登录(SSO)系统!
像 OAuth 或 SAML 这样的 SSO 流程通常是这样的:
- 用户在鉴权中心(比如 Google 登录页面)验证身份。
- 鉴权中心向用户的浏览器写入一个 Session Cookie。
- 鉴权中心立刻通过一个 跨站 POST 请求(带着刚才的 Cookie),把用户重定向回原本的业务系统。
如果浏览器死板执行Lax规则,那么上述登录系统就永远无法执行登录逻辑,为了妥协这个过程,同时为了给全网的开发者留出修改代码的时间,Chrome 引入了一个名为 Lax + POST mitigation(Lax POST 缓解机制)的例外规则:
如果一个 Cookie 是在"两分钟内"刚刚被设置的,那么浏览器会暂时豁免它的 Lax 限制。在这个极其短暂的 2 分钟窗口期内,这个 Cookie 会被当作 SameSite=None 来对待,允许它在跨站的顶级 POST 请求中被发送。
所以通过这样的方式,我们可以在Cookie刷新的2分钟内,执行CSRF,从而在SameSite=None 执行修改邮件的请求。
执行过程: 其实本质仍然是通过恶意链接注入,并且我们可以直接提交POST请求,但是需要在目标用户Cookie刷新2分钟内执行,我们可以在执行注入恶意请求前,先让想办法让Cookie刷新,可以利用一些Social Login之类的功能,让用户先请求这个功能,刷新Cookie,在短暂延迟后注入恶意请求,从而达成修改邮箱。
首先进入环境,登录账户,可以看到我们会通过一个额外的OAuth登录,当我们在这个额外的登录系统中验证完身份后,登录系统会将我们的Cookie下发给我们,带回原网站从而执行登录逻辑。
javascript
<form method="POST" action="https://target.com/my-account/change-email" id="csrf-form">
<input type="hidden" name="email" value="Leo@gmail.com">
</form>
<script>
// 监听整个页面的点击事件
window.onclick = () => {
// 动作一:打开一个新窗口(或标签页),强制受害者访问鉴权端点刷新 Cookie
// 这一步会触发后端的 Set-Cookie 响应
window.open('https://target.com/social-login');
// 动作二:设置一个 5 秒的定时器。
// 等待 5 秒是为了给新窗口完成网络请求和重置 Cookie 留出充足的时间。
// 5 秒后,在当前页面提交 POST 表单。
// 由于此时 Cookie 刚刚刷新(< 2分钟),Chrome 的宽限期规则生效,请求成功带上 Cookie!
setTimeout(() => {
document.getElementById('csrf-form').submit();
}, 5000);
}
</script>
<h1>Click anywhere on this page to claim your prize!</h1>
我们可以将上述代码提交到漏洞攻击服务器中,模拟访客触发恶意页面,当单击后执行上述恶意逻辑时,会先去请求目标的social-login的来刷新目标的Cookie,在延迟5秒后再去请求修改邮件的请求,从而在这个SameSite = none的间隙中注入恶意请求。

这是恶意执行过程,先执行攻击/exploit,在执行social-login刷新Cookie,延迟后执行最终的change-email从而实现修改邮箱。
总结
CSRF这类问题,往往会伴随着浏览器、Web架构等等技术发展而变化,其实核心机制仍然是当整个系统仅仅依靠Cookie来认证用户时,发生的纰漏性问题,本文借助几个Lab来展现了面对SameSite的限制下,常见的CSRF的表现形式。