同源策略 ≠ 万能盾牌:为什么你的后端仍需防范"盲打"攻击?

62 阅读4分钟

一个常见的认知误区:很多开发者认为浏览器的同源策略(Same-Origin Policy)已经阻止了跨域攻击,因此忽略了后端对请求来源的校验。然而事实残酷——同源策略只管"读",不管"写"。恶意网站完全可以在你不知情的情况下,用你的身份执行转账、删号等高危操作。

一、同源策略的"盲区":它只管数据泄露,不管数据篡改

当我们谈到浏览器安全时,同源策略(SOP)总是被首先提及。它确实有效地防止了以下场景:

// 你在 a.com 登录了银行
// a.com 的 JS 试图读取 b.com 的数据
fetch('https://b.com/api/balance')
  .then(res => res.json())
  .then(data => console.log(data)); // ❌ 被浏览器拦截:CORS policy blocked

但是,如果我们换个写法——不关心服务器返回什么,只让服务器执行操作:

// 恶意网站 evil.com 的代码
fetch('https://bank.com/transfer?to=hacker&amount=10000', {
  method: 'POST',
  // 甚至不需要处理响应...
});

会发生什么?

  1. ✅ 请求确实发出去了(带上了你的 Cookie)
  2. ✅ 银行服务器已经执行了转账(钱没了)
  3. ✅ 响应也回到了浏览器(你可以在 DevTools 里看到 200 OK)
  4. JS 拿不到响应数据(同源策略只拦住了这一步)

这就是CSRF(跨站请求伪造)攻击的核心:恶意网站不需要知道你的余额,它只需要让你的浏览器去执行操作

二、CSRF 攻击实战模拟

想象这样一个场景:

上午 9:00:你在银行网站 bank.com 登录,浏览器保存了 Session Cookie。

上午 10:00:你没关浏览器,打开了钓鱼邮件里的链接 evil.com

evil.com 的 HTML(极其简单,甚至不需要 JS):

<body>
  <h1>精美壁纸下载中...</h1>
  <!-- 图片自动加载,触发 GET 请求 -->
  <img src="https://bank.com/transfer?to=attacker&amount=50000" width="0" height="0" />
  
  <!-- 或者自动提交的表单 -->
  <form id="csrf" action="https://bank.com/change-email" method="POST">
    <input type="hidden" name="email" value="hacker@evil.com" />
  </form>
  <script>document.getElementById('csrf').submit();</script>
</body>

结果:你的银行卡转账 5 万元,或者绑定邮箱被改成了黑客的。整个过程不需要 JavaScript 读取任何返回数据,同源策略对此完全无能为力

三、后端校验:守护最后一道防线

既然浏览器"靠不住",服务端必须承担校验责任。目前业界主流的防御方案有三层:

1. CSRF Token(最经典可靠)

原理:在表单或请求头中嵌入一个随机生成的、只有服务器和当前页面知道的 Token。

<!-- 银行页面渲染时生成随机 Token -->
<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="dW5pcXVlLWhhc2gtdmFsdWU=" />
  <input name="amount" value="1000" />
  <button>转账</button>
</form>
// Spring Boot 示例
@PostMapping("/transfer")
public String transfer(@RequestParam String amount, 
                      @RequestParam String csrf_token,
                      HttpSession session) {
    // 校验 Token 是否与会存中存储的一致
    if (!csrf_token.equals(session.getAttribute("csrf_token"))) {
        throw new SecurityException("疑似 CSRF 攻击!");
    }
    // 执行转账...
}

关键:恶意网站 evil.com 无法读取 bank.com 的页面内容(同源策略这时候起作用了!),因此拿不到 Token,请求自然被后端拒绝。

2. SameSite Cookie(现代浏览器的防线)

通过设置 Cookie 属性,让浏览器不在跨域请求中携带敏感 Cookie:

Set-Cookie: sessionId=abc123; SameSite=Strict; HttpOnly; Secure
  • SameSite=Strict:完全禁止第三方 Cookie(最安全,但可能影响从微信/邮件点击链接的体验)
  • SameSite=Lax:允许顶级导航(如 <a> 标签跳转)携带 Cookie,但阻止 POST/PUT 等危险操作(推荐折中方案)

3. Origin/Referer 校验(请求来源验证)

检查 HTTP Header 中的来源信息:

@PostMapping("/sensitive")
public ResponseEntity<?> sensitiveOperation(@RequestHeader("Origin") String origin) {
    if (!"https://bank.com".equals(origin)) {
        return ResponseEntity.status(403).body("非法来源");
    }
    // 继续处理...
}

注意:Referer 可以被抹除(HTTPS 跳 HTTP 时不带),所以优先校验 Origin 头,同时不能作为唯一防线

四、总结:两道门的安全哲学

理解同源策略和 CSRF 防护的关系,可以用一个比喻:

同源策略阅览室的保安:防止外人偷看你的文件(保护数据隐私)。 CSRF 防护金库的指纹锁:确保操作者真的是你本人(保护操作安全)。

两者缺一不可

  • 没有同源策略,坏人能直接读取你的敏感信息(余额、 Token)。
  • 没有 CSRF 防护,坏人能盲打执行高危操作(转账、改密),即使你收不到回执。

给开发者的建议

  1. 高风险操作(资金、隐私修改)必须实施 CSRF Token 验证,不能仅靠前端校验。
  2. 逐步淘汰纯 Cookie 鉴权,采用 Authorization: Bearer Token 方案(Token 不自动携带,需 JS 显式写入 Header,天然防 CSRF)。
  3. 开启 SameSite=Lax,作为兜底防护。

同源策略解决了"谁能看到"的问题,但"谁能操作"必须由你的后端来回答。别让浏览器的"好意"成为安全架构的盲点。