学习同源策略和CORS(Cross-Origin-Resource-Sharing)(跨域资源共享)的历史的演变,深入理解CORS和不同类型的跨域访问策略以及一些最好的例子。

文章目录

  • 你的浏览器控制台报错了
  • 起点:第一个子资源标签
    • 域与跨域
    • 跨域请求的危害
  • 同源策略
  • 进入CORS
    • 跨域写
    • 预请求
    • 跨域读
  • 如何使用好CORS
  • 一些好例子
    • 所有请求都允许
    • Keeping it in the family
    • NULL域
    • 跳过Cookies,如果你能的话
  • 附加阅读
  • 后记

你的浏览器控制台报错了




我确定你在浏览器的控㓡台见过上面的报错或者它们的变体。如果你没有见过,不用担心,你总会遇到的。对于所有开发人员来说,遇到CORS错误是常事。

在开发过程中遇到些种问题可能是烦人的,但实际上,在一个配置异常的web服务器上,网络上的攻击者或者推动网络标准的组织来说,CORS是一种非常有用的机制。

首先,让我们回到起点。

起点:第一个子资源标签

子资源标签是一个请求被嵌入到文档或者一个请求在上下文中被执行的HTML标签。在1993年,第一个子资源标签 <img> 出现了。为了兼容 <img> 标签,网络页面变得更漂亮了,但也更复杂了。


所以,如果你的浏览器想要渲染一个带有 <img> 标签的网页,它就必须从域中请求子资源。当浏览器请求的子资源与域在协议,主机名和端口上任何一个不同时,这就是一个跨域请求

域与跨域

域被从三个方向定义:协议,完整的主机名和端口。例如:http://example.comhttps://example.com 是不同域的。第一个使用 http 协议,而第二个使用 https 协议。而且默认的 http 协议使用80端口,而 https 协议使用443端口。所以在这个例子中,这两个域因为协议和端口不同而不周域,尽管它们的主机名是一样的。

同理,你知道如果三个条件中其中有一个不同,那域就是不同的。

下面是我们总结与 https://blog.example.com/posts/foo.html 域同源或者跨域结果的例子:

URL 结果 原因
https://blog.example.com/posts/bar.html 同源 只有路径不同
https://blog.example.com/contact.html 同源 只有路径不同
http://blog.example.com/posts/bar.html 不同源 协议不同
https://blog.example.com:8080/posts/bar.html 不同源 端口不同( https:// 默认使用443端口)
https://example.com/posts/bar.html 不同源 主机名不同

其中一个跨域请求例子:一个来自 http://example.com/posts/bar.html 页面想请求从 https://example.com 来的子资源。

跨域请求的危害

现在我们知道了什么是同源和跨域,让我们看看会发生什么大事。

当我们介绍出现了第一个子资源标签 <img> 的时候,我们就已经打开了闸门,之后我们有了 <script><frame><video><audio><iframe><link><form> 等等更多的子资源标签。这些子资源标签都可以发出同源或者跨域请求。

我们想像一个CORS不存在而且所有跨域请求都被允许的世界。

想像这样一个场景:我从 evil.com 得到一个带 <script> 标签的页面,从表面上看,这是一个简单的页面,能显示一些有用的信息。但是在 <script> 标签里面有我精心设计的代码,会发送一个精心设计的 DELETE /account 请求到银行的服务器。你只要加载一次这个页面,JavaScript脚本就会被执行,异步请求就会调用银行的API。

惊喜不惊喜?你只要在网页上浏览一些信息,你就能收到成功删除银行账号的邮件。事实上我猜它能向银行做任何事情。

为了让我邪恶的 <script> 标签能工作,在请求中你的浏览器还必须带从银行网站得到的凭证(cookies)。这是为什么银行服务器能认识你并知道删除哪个账号的原因。

现在让我们看一个不那么邪恶的例子。

我想查看一个在Awesome Corp工作的朋友,他工作的内网是 intra.awesome-corp.com 。在我的网站 dangerous.com 我有一个 <img src="https://intra.awesome-corp.com/avatars/john-doe.png"> 标签。

未能与内网 intra.awesome-corp.com 建立有效会话的用户,头像将不会渲染,会出现一个错误。但是,当你登录内网,访问过一次我的网站,我就知道你建立了有效会话。

这意味着我能得到你的一些信息。对我来说直接攻击可能比较困难,但是知道你登录Awesome Corp仍然是一个潜在的攻击媒介。


这两个过于简单的例子显示出的威胁说明了同源策略和CORS的必要。跨域请求的危险多种多样。一些可以缓解,另一些则是不能缓解的-它建立在网络的本质上。因为CORS,大量的攻击媒介被压缩。

在了解CORS之前,我们要先了解一下同源策略。

同源策略

同源策略可以通过阻止不同源的资源加载来预防跨源攻击。但是这个策略还是允许一些标签如 <img> 嵌入不同源的资源的。

同源策略每一次出现在1995年的2.02版本的网景浏览器(Netscape Navigator)上。源头是为了保护跨源访问DOM。

虽然同源策略没有明确的标准,但是所有现代浏览器都以某种形式实现了它,最后IETF在RFC6454中定义了同源策略的标准。

同源策略被定义成了下面的规则集:

标签 跨域策略 注意
<iframe> 允许嵌入 取决于 X-Frame-Options
<link> 允许嵌入 属性 Content-Type 可能需要
允许写

同源策略解决了很多问题,但是也有点严格了。从单页应用到重媒体网站,同源策略并没有留下足够的余地去调整这些规则。

CORS出生的目标就是调整同源策略以适应不同的跨域访问。

进入CORS

现在我们知道了源的定义,跨域请求的缺点和浏览器实现的同源策略。

是时候了解CORS(跨域资源分享)了。CORS是一种允许控制通过网络访问网页上的子资源的机制。这种机制定义了三种子资源访问策略:

  • 跨域写
  • 跨域嵌入
  • 跨域读

在我们解释每一种策略之前,需要意识到非常重要的一点是:浏览器允许某种类型的跨域请求,并不代表服务器会接受这类请求。

跨域写 代表链接,重定向和表单提交。当你的浏览器开启CORS时,这些都是被允许的。而且还有一种叫预请求的机制对跨域写进行调整。所以默认允许进行跨域写也并不代表它能成功。我们会在稍后讨论它。

跨域嵌入代表子资源加载,如:<script><link><img><video><audio><object><embed><iframe> 等等。这些默认都被允许<iframe> 比较特殊——它的目的是嵌入另外的页面,是否允许嵌入被 X-Frame-options 请求头控制。

当是其它标签时,它们很自然的会触发跨域请求。这是为什么CORS要区分跨域读和跨域嵌入的原因。

跨域读代表通过调用AJAX或者 fetch 函数加载子资源。它们在你的浏览器中默认是被禁止的。这是在页面中嵌入子资源的一个解决方法。但是这种方法在现代浏览器中被另一个策略控制。

如果你的浏览器是最新的,所有的这些策略应该已经都实现了。

跨域写

跨域写可能是问题的。让我们通过例子写实际看看CORS。

首先,我们设置一个简单的Crystal HTTP服务器(使用 Kemal)(我自己使用express服务实现了一个相同的):

require "kemal"port = ENV["PORT"].to_i || 4000get "/" do"Hello world!"
endget "/greet" do"Hey!"
endpost "/greet" do |env|name = env.params.json["name"].as(String)"Hello, #{name}!"
endKemal.config.port = port
Kemal.run

这一个简单的请求,路径是 /greet ,有一个name参数,返回一个 Hello #{name}! 字符串。为了运行这个服务器,你可以用下面的命令:

$ crystal run server.cr

它将会启动服务到 localhost:4000 。如果现在我们使用浏览器访问这个路径,会得到一个简单的 “Hello World”页面。

Hello, world! 												<div id=