Web 安全

CORS (跨源资源共享)

约 4 分钟阅读

什么是 CORS (跨源资源共享)

CORS (Cross-Origin Resource Sharing) 是一种基于 HTTP 头的机制,用于安全地放宽浏览器的同源策略,实现不同源之间的资源共享。源由协议 (http/https)、主机名和端口号三个要素的组合定义。

同源策略是浏览器的安全机制,限制从一个源加载的脚本读取另一个源的响应。如果没有这个限制,恶意网站就可以通过用户的浏览器调用用户已登录的银行网站的 API,并用自己的脚本读取响应中包含的余额和交易记录。

这里需要理清的关系是:默认进行防护的是同源策略一侧,而 CORS 是用于有选择地放宽该限制的机制。把它理解为「配置 CORS 来阻止跨源攻击」,方向正好相反,实际上这是把默认关闭的东西只按必要范围打开的操作。还有一点,同源策略阻止的是响应的读取,而不是请求的发送。发送本身仍然会成立,这正是 CSRF 至今仍是现实威胁的原因。

然而,在现代 Web 应用中,前端和后端 API 运行在不同域名上是很常见的。CORS 通过服务器在 HTTP 响应头中明确声明「允许哪些源的访问」,安全地实现必要的跨源通信。

CORS 的工作机制

CORS 根据请求类型以两种方式工作。

简单请求 (Simple Request)

当方法为 GET、HEAD、POST 之一,附加的头处于 Accept、Accept-Language、Content-Language、Content-Type 这类安全列表的范围内,且 Content-Type 的值为 application/x-www-form-urlencodedmultipart/form-datatext/plain 之一时,该请求被视为「简单请求」。反过来说,只要多加一个 AuthorizationX-Requested-With 这样的自定义头,就会脱离这个条件。

在简单请求中,浏览器直接发送请求,并检查响应中的 Access-Control-Allow-Origin 头。如果源被允许,则将响应传递给 JavaScript;否则阻止读取。被阻断的只是来自 JavaScript 的读取,请求已经到达服务器,处理也已经执行。即使浏览器控制台显示 CORS 错误,服务器端也可能已经登记或更新了数据,在开发过程中排查问题时务必意识到这个区别。

预检请求 (Preflight Request)

对于不满足简单请求条件的请求 (如 PUT、DELETE 或包含自定义头的请求),浏览器在实际请求之前使用 OPTIONS 方法发送「预检请求」。服务器通过以下头响应允许条件。

  • Access-Control-Allow-Origin:允许的源
  • Access-Control-Allow-Methods:允许的 HTTP 方法
  • Access-Control-Allow-Headers:允许的请求头
  • Access-Control-Max-Age:预检结果的缓存时间 (秒)

如果预检响应满足允许条件,浏览器才发送实际请求。

携带凭据的请求

包含 Cookie 或认证头的跨源请求需要 Access-Control-Allow-Credentials: true。此时,Access-Control-Allow-Origin 不能使用通配符 (*),必须指定具体的源。通配符仅对不携带凭据的请求有效,这是规范层面的互斥,不存在让两者同时成立的写法。Access-Control-Allow-MethodsAccess-Control-Allow-Headers 中的 * 也一样,在携带凭据的请求中不会被当作通配符。

另一个陷阱是预检本身不携带 Cookie (规范规定预检的凭据模式始终为 same-origin)。如果在服务器或反向代理的配置中对 OPTIONS 请求也要求认证,就会在到达实际请求之前失败,形成原因难以看清的 CORS 错误。

常见配置错误与安全风险

CORS 配置错误会使同源策略的保护失效,造成严重的安全风险。

  • 随意使用 Access-Control-Allow-Origin: *:允许所有源意味着任何网站都可以访问 API。除公开 API 外不应使用。处理认证信息的 API 绝对不能使用
  • 未经验证直接反映 Origin 头:将请求的 Origin 头值直接反映到 Access-Control-Allow-Origin 中,实质上等同于通配符。应通过白名单验证
  • 允许 null 源:允许 Access-Control-Allow-Origin: null 会使沙箱化的 iframe 和本地文件可以访问,可能被攻击利用
  • 过于宽松的子域许可与后缀匹配:使用 *.example.com 这样的模式匹配时,如果攻击者能够接管子域,就可以利用 CORS。更进一步,只检查字符串结尾是否一致的实现,还会放行攻击者自己注册的 evil-example.com 这类另外的域名

CORS 配置错误与 XSSCSRF 结合时,可能扩大危害。另一方面,即使把 CORS 配置得再严格也无法防止 CSRF。像表单提交这样的简单请求不会触发预检就能到达服务器,因此 CSRF 需要用令牌或 Cookie 的 SameSite 属性另行防护。

安全 CORS 配置的最佳实践

安全配置 CORS 的指南。

  • 通过白名单管理允许的源:在环境变量或配置文件中维护允许源列表,与请求的 Origin 头进行精确匹配验证
  • 仅允许最少必要的方法和头:在 Access-Control-Allow-MethodsAccess-Control-Allow-Headers 中只列出实际使用的内容
  • 利用预检缓存:明确设置 Access-Control-Max-Age,减少预检请求的频率。未指定时规范上的默认值只有 5 秒,实际上几乎每个请求之前都会发出一次 OPTIONS。不过浏览器端存在缓存时间的上限,超过上限的值会被削减到上限,因此写极长的秒数也不会延长效果
  • 谨慎处理携带凭据的请求:设置 Access-Control-Allow-Credentials: true 时,严格限制允许的源
  • CSP安全头配合使用:将 CORS 与其他安全头结合,构建纵深防御
  • HTTPS 为前提:不在允许列表中包含 HTTP 源。防止通过中间人攻击伪造源

CORS 是浏览器端的控制,不适用于服务器间通信或 curl 这类命令行工具的请求。对于攻击者从自己的环境直接调用 API 的路径没有效果,因此不要仅依赖 CORS,务必实现服务器端的认证和授权。

另外,CORS 先是作为 W3C 建议规范「Cross-Origin Resource Sharing」(2014 年 1 月 16 日) 标准化,之后被并入 WHATWG 的 Fetch 标准。确认细节行为时,参照 Fetch 标准更为准确。

常见误解

CORS 是保护服务器的安全功能
CORS 是浏览器端的控制,不直接保护服务器。curl 或服务器间通信不受 CORS 限制。本来禁止读取其他源响应的是同源策略,CORS 是放宽该限制一侧的机制。它是保护浏览器用户的一层,与服务器端的认证和授权处于不同层面。
出现 CORS 错误用通配符 (*) 解决就好
通配符允许所有源的访问,安全风险很高。携带凭据的请求不能使用通配符。正确的做法是仅将必要的源添加到白名单中。
分享

相关术语

相关文章