再也不会对 CORS 感到沮丧。了解什么是跨域资源共享、它存在的原因以及如何接受它。
CORS 或跨域资源共享是一种可选的浏览器功能,网站可以使用它以受控方式放宽同源策略。
浏览器通过标头促进 CORS Access-Control-Allow-*
,我们将很快介绍。
我不想让你对 CORS 感到沮丧,所以让我们先介绍一点理论。具体来说,我们来看看同源策略。
我已经在此处详细介绍了这一点,但为了给您提供 TL;DR,同源策略是一组管理 Web 浏览器功能实现方式的设计原则。
其目的是将浏览器窗口(和选项卡)彼此隔离。
例如,当您访问 example.com 时,该网站将无法阅读来自 gmail.com 的电子邮件(您可能在另一个选项卡中打开了该电子邮件)。这是由于同源策略的工作原理。
起源的定义很简单。如果两个网站的方案(http://、https:// 等)、主机(例如www.appsecmonkey.com)和端口(例如 443)相同,则它们的来源相同。您可以在RFC6545 - The Web Origin Concept中找到定义。
如果未明确指定端口,则默认为 80http
和 443 https
。
浏览器认为这些 URL 具有相同的来源:
https://www.appsecmonkey.com/
https://www.appsecmonkey.com/blog/same-origin-policy/
https://www.appsecmonkey.com:443/blog/same-origin-policy/
这些都是不同的起源:
http://www.appsecmonkey.com/
https://appsecmonkey.org/
https://www.appsecmonkey.com:8080/
一般来说,允许写入和嵌入,而拒绝读取。具体如何应用取决于浏览器功能,但这里有一些特别涉及 CORS 的示例。我们可以将示例分为两类:
以下将起作用。你会得到一个错误,但是请求会被发送。您可以使用浏览器的开发人员工具进行验证,或者更好的是,在您的浏览器和网络服务器之间设置一个代理工具,例如OWASP ZAP,以查看发生了什么。
- let xhr = new XMLHttpRequest()
- xhr.withCredentials = true
- xhr.open('GET', 'http://b.local/')
- xhr.send()
开发者工具显示浏览器确实向服务器发送了以下 HTTP 请求。
- GET / HTTP/1.1
- Host: b.local
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:86.0) Gecko/20100101 Firefox/86.0
- Accept: */*
- Accept-Language: en-US,en;q=0.5
- Accept-Encoding: gzip, deflate
- Origin: http://a.local
- Connection: keep-alive
- Referer: http://a.local/
- Cookie: SESSIONID=s3cr3t
- Pragma: no-cache
- Cache-Control: no-cache
但是,您将无法阅读收到的回复。这是在行动的同源策略。允许写入(发送 XHR 请求),但不允许读取响应。
❌ Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://b.local/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
使用 fetch 也一样。我们可以使用 JavaScript 向网络服务器提交一个 URL 编码的表单。
- fetch('http://b.local/', {
- method: 'POST',
- credentials: 'include',
- body: 'foo=bar',
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
- },
- })
并发送以下 HTTP 请求:
- POST / HTTP/1.0
- Host: b.local
- User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:86.0) Gecko/20100101 Firefox/86.0
- Accept: */*
- Accept-Language: en-US,en;q=0.5
- Accept-Encoding: gzip, deflate
- Referer: http://a.local/
- Content-Type: application/x-www-form-urlencoded;charset=UTF-8
- Origin: http://a.local
- Content-Length: 7
- Connection: keep-alive
- Cookie: SESSIONID=s3cr3t
- Pragma: no-cache
- Cache-Control: no-cache
fetch 也是一样。允许发送经过认证的跨域 POST 请求,但拒绝访问响应。
❌ Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://b.local/. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).
我们将Access-Control-Allow-Origin
在一分钟内了解事情是什么,但让我们先看几个场景。
默认情况下只允许特定的 HTTP 动词(GET、POST、HEAD 和 OPTIONS)。
- fetch('http://b.local/', {method: 'PUT', credentials: 'include'});
- ❌ Access to fetch at 'http://b.local/' from origin 'http://a.local' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
请注意,这次浏览器根本没有发送请求。另外,请注意,错误现在不同了。它正在谈论预检请求。我们很快就会做到这一点。
同源策略不允许尝试发送带有任意标头的请求。
- fetch('http://b.local/', {
- method: 'POST', credentials: 'include', headers: {
- 'Foo': 'Bar'
- },
- });
- ❌ Access to fetch at 'http://b.local/' from origin 'http://a.local' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
默认情况下,仅允许使用CORS安全列表的标头。他们是:
虽然Content-Type
标头被列入安全列表,但也有限制。具体来说,只有以下值是可接受的:
- fetch('http://b.local/', {
- method: 'POST', credentials: 'include', headers: {
- 'Content-Type': 'application/json'
- },
- });
- ❌ Access to fetch at 'http://b.local/' from origin 'http://a.local' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
再次与预检。
好的,既然您了解了同源策略对您的限制,我将告诉您什么是 CORS,以及它如何帮助您以可控的方式绕过这些限制。
CORS 可以解除上述限制。它不是同源策略那样的浏览器安全机制。CORS 是一种浏览器不安全机制,因此请仔细阅读并谨慎使用。
浏览器将 CORS 实现为一组四个 HTTP 响应标头,我们现在将介绍。
第一个标题是Access-Control-Allow-Origin
. 开发者可以使用它来授予跨域请求对网站资源的读取权限(默认情况下同源策略拒绝)。
可能的值为:
https://www.appsecmonkey.com
例如:
Access-Control-Allow-Origin: https://wwww.appsecmonkey.com
或者:
Access-Control-Allow-Origin: *
这是非此即彼。这样的事情是行不通的:
Access-Control-Allow-Origin: *.appsecmonkey.com
此外,不允许指定多个来源,因此以下内容也不起作用。
Access-Control-Allow-Origin: https://foo.appsecmonkey.com, https://bar.appsecmonkey.com/
CORS 通配符限制
通配符不能与 结合使用Access-Control-Allow-Credentials: true
,稍后您将了解。只要记住这个限制。
如何指定多个 CORS 来源?
该规范不支持多个来源。但是,实际上,它已通过Access-Control-Allow-Origin
在代码中动态生成标头来解决。请求标头促进了此解决方法Origin
,浏览器在所有 POST 和 CORS 请求中发送该标头。
请注意,这里存在安全隐患。最好为您选择的开发框架使用完善的 CORS 库,而不是实施自制的解决方案。
无论如何,您应该有一个严格的可能来源白名单。不要实现例如检查请求源是否包含特定字符串或以字符串开头的逻辑。
默认情况下,CORS 不允许有凭据的请求(例如,包括浏览器用户的 cookie 或客户端证书)。毕竟,经过认证的 CORS 请求有效地赋予了被授予权限的网站对应用程序中浏览器用户数据的完全读写控制。
如果您仍想启用它,您可以Access-Control-Allow-Credentials
像这样使用标头:
Access-Control-Allow-Credentials: true
请注意上面提到的限制:这不能与Access-Control-Allow-Origin: *
.
关于 SameSite cookie 的重要警告
浏览器开始默认采用 Lax 模式下的 SameSite cookie。我在这里详细写了有关 SameSite cookie 的文章。
这意味着默认情况下,cookie 受到保护,不受跨站点交互的影响。如果您想弄清楚为什么即使您拥有所有正确的标头,浏览器也不发送您的cookie,那可能是SameSite cookie在起作用。
如果您想发送自定义标头或解除对Content-Type
标头的限制,例如发送 JSON 请求,您可以使用Access-Control-Allow-Headers
来执行此操作。
Access-Control-Allow-Headers: content-type
最后,假设您想要启用除 GET、POST、HEAD 和 OPTIONS 之外的其他 HTTP 动词。在这种情况下,您必须使用Access-Control-Allow-Methods
标题。
Access-Control-Allow-Methods: GET, POST, HEAD, PUT, PATCH, DELETE
事实上,即使你只想允许,例如 POST 请求,Access-Control-Allow-Methods
如果有任何其他因素导致你的请求被预检,你仍然需要返回,我们将在下面讨论。
始终发送带有列入白名单的 HTTP 动词、标头和内容类型的简单请求。尽管如此,如果响应不包含适当的Access-Control-Allow-Origin
标头,则禁止网站访问响应数据。
但是浏览器如何知道是否允许发送PUT 请求呢?如果“我可以发送 PUT 请求”这个问题的答案是对 PUT 请求的响应,那么这不会造成先有鸡还是先有蛋的问题吗?这是一个很好的问题,答案很简单:我们发送两个请求。
浏览器首先发送OPTIONS
带有标头的请求Origin
,然后查看该请求的响应标头。如果PUT
允许(在 中Access-Control-Allow-Methods
),则只有预检成功,并且浏览器发送所需的 PUT 请求。
当然还有其他原因导致预检失败。例如,您可能尝试发送包含凭据的请求,但 Web 服务器未Access-Control-Allow-Credentials: true
在预检 HTTP 响应中返回。
第一个 OPTIONS 请求被恰当地命名为preflight request。
让我们回顾一下我们之前所做的一项测试,但这一次,http://b.local/返回以下 HTTP 响应标头:
- Access-Control-Allow-Credentials: true
- Access-Control-Allow-Headers: content-type
- Access-Control-Allow-Methods: GET, HEAD, POST, PUT
- Access-Control-Allow-Origin: http://a.local
现在我们已经完全控制了跨域页面。
- fetch('http://b.local', {method: 'PUT', credentials: 'include', headers: {
- 'Content-Type': 'application/json'
- }}).then(function (response) {
- return response.text();
- }).then(function (html) {
- // This is the HTML from our response as a text string
- console.log(html);
- });
-
-
-
-
b.local - ...
☠ 安全影响:如果您指定这样的 CORS 标头,您将给予允许的来源完全控制您的网站,包括任何经过身份验证的用户数据和功能。同源政策可以保护您,因此在选择退出之前请仔细考虑。
如果您的要求很简单,您只需将静态标头添加到您的应用程序/Web 服务器配置中。
但是,如果您必须处理多个来源,最好为您的开发框架使用 CORS 库。例如,这是在烧瓶中配置 CORS 的方式:
- CORS(
- app,
- origins=['http://a.local', 'http://c.local'],
- allow_headers=['content-type'],
- supports_credentials=False,
- methods=['PUT', 'PATCH', 'DELETE']
- )
默认情况下,只有CORS 安全列表中的响应标头会暴露给 CORS 请求中的 JavaScript 代码。因此,如果您的网络服务器返回 header Foo: Bar
,即使 CORS 请求也无法访问它。
如果您希望浏览器访问此标头,可以通过以下Access-Control-Expose-Headers
方式进行:
Access-Control-Expose-Headers: Foo
如果两个网站具有相同的方案、主机和端口,则浏览器会认为它们具有相同的来源。
同源策略对跨源交互施加了一些限制。最值得注意的是 CORS:
CORS 或跨域资源共享是一种浏览器不安全机制,可让您的 Web 应用程序以受控方式选择退出其中一些限制。
但是,应该考虑使用 CORS,并使用完善的 CORS 库来实现您选择的开发框架。