浏览器 Web 安全基础系列

  1. 浏览器怎么判断是不是同一个网站

  2. Cookie 为什么会自动携带

  3. Session 登录状态为什么可能被劫持

  4. HTTPS 为什么仍然不够

  5. CORS 到底在限制什么(当前篇)

  6. CSRF 是怎么冒用登录状态的

  7. CSRF 如何防御

  8. XSS 为什么能在你的网站执行

  9. CSP 与浏览器安全响应头

  10. Clickjacking 点击劫持

  11. 第三方资源为什么危险

  12. 防盗链到底防什么

前端页面从 http://localhost:3000 请求 http://localhost:4000/api,控制台出现 CORS 错误。很多人因此认为“请求没有发出去”。实际上,CORS 主要控制浏览器是否把跨源响应交给 JavaScript,部分请求可能已经到达服务器并执行。

CORS 简单请求

CORS 是服务器给浏览器的许可

前端发起:

fetch('https://api.example.com/users');

浏览器在请求中带上:

Origin: https://www.example.com

服务端允许该来源时返回:

Access-Control-Allow-Origin: https://www.example.com

浏览器看到允许头,才把响应正文交给 JavaScript。curl 和后端服务不会执行这个检查,因为 CORS 是浏览器安全模型的一部分。

简单请求

某些满足条件的 GET、HEAD、POST 请求可以直接发送,不先预检。例如普通表单格式 POST:

fetch('https://api.example.com/search', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'q=cors'
});

“简单”不表示安全,也不表示服务端不用鉴权。它只是 Fetch/CORS 规范对请求形态的分类。

预检请求

如果使用 DELETE、自定义请求头或不属于简单类型的 Content-Type,浏览器通常先发 OPTIONS:

OPTIONS /users/1
Origin: https://www.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization

服务端回答:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization

允许后,浏览器再发送真正请求。

CORS 预检流程

Express 手写示例:

const allowedOrigins = new Set(['https://www.example.com']);

app.use((req, res, next) => {
  const origin = req.headers.origin;

  if (origin && allowedOrigins.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  }

  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

Vary: Origin 用来提醒缓存:响应会因 Origin 不同而不同,避免代理错误复用 CORS 响应。

前端必须明确允许凭证:

fetch('https://api.example.com/profile', {
  credentials: 'include'
});

服务端返回:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true

此时不能使用:

Access-Control-Allow-Origin: *

因为允许任意来源同时读取带凭证响应会破坏安全边界。

还要注意:credentials: include 只表示 Fetch 可以使用凭证,Cookie 是否真正发送仍要经过 Domain、Secure、SameSite 和第三方 Cookie 策略判断。

为什么 CORS 不能防 CSRF

CSRF 攻击通常不需要读取响应,只需要让状态修改请求到达服务器。HTML 表单可直接跨源提交:

<form action="https://bank.example/transfer" method="post">
  <input name="to" value="attacker">
  <input name="amount" value="1000">
</form>

即使银行没有给攻击网站配置 CORS,表单请求仍可能发送。攻击页面读不到返回页面,但转账已经执行。

因此:

CORS:谁能通过浏览器 JavaScript 读取响应
CSRF 防护:谁有资格让服务端执行状态修改

CORS 不是权限系统

把攻击者 Origin 从白名单移除,不能替代登录和资源授权。攻击者可以用自己的服务器、curl 或脚本直接请求 API,根本不受浏览器 CORS 约束。

每个接口仍需验证:

  • 身份认证;

  • 用户是否有权访问目标资源;

  • 输入是否有效;

  • 状态修改是否需要 CSRF 防护;

  • 请求频率是否合理。

常见错误

反射任意 Origin

危险配置:

res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');

它等于允许任意网站读取带用户凭证的响应。

白名单使用 endsWith

origin.endsWith('example.com')

可能错误接受 notexample.com。应解析 URL 后精确匹配完整 Origin。

所有接口都允许所有方法和请求头

按业务实际需要最小化开放范围,尤其是凭证请求。

小结

CORS 是浏览器跨源读取许可,不是防火墙,也不是身份认证。理解“请求可能已发送,但响应不交给 JavaScript”,就能区分 CORS、同源策略和 CSRF。

参考资料