浏览器的同源策略
同源策略是浏览器的一种安全策略,用来限制一个源与另一个源上的文档和脚本的交互。
浏览器可以一次加载多个站点的资源,一个站点可以嵌入来自不同站点的多个 iframe,如果这些资源之间的交互没有限制,并且脚本被攻击者破坏,则脚本可能暴露用户的一切信息。
同源策略通过阻止对不同源的资源的读取访问来防止这种情况的发生。但是浏览器允许一些标签来嵌入来自不同源的资源(图像、脚本······),它可能使你的站点暴露,例如使用 iframe 进行点击劫持,你可以使用内容安全策略限制对这些标签的跨源读取。
同源:协议、域名、端口都相同被认为是同源的。
允许 & 阻止
通常,允许嵌入跨域资源而阻止读取跨域资源。
| 资源 | 策略 |
|---|---|
iframes | 通常允许跨域嵌入(取决于 X-Frame-Options 指令),但不允许跨域读取(使用 JavaScript 访问 iframe 中的文档)。 |
| CSS | 跨源 CSS 可以使用 link 标签或者通过 @import 导入 CSS 文件,但是需要设置正确的 Content-Type 属性。 |
| forms | 跨源 url 可以,action 属性的值,Web 应用程序可以将表单数据写入另一个源。 |
| images | 允许嵌入跨源图像,但是不允许读取跨源图像(在 canvas 使用 JavaScript 将跨源图像加载到元素中)。 |
| multimedia | 可以使用<video>和<audio>元素嵌入跨源视频和音频。 |
| script | 可以嵌入跨源脚本,但是某些 API 的访问会被阻止。 |
Cookie、localStorage、indexDB无法读取。- DOM 无法获取。
- AJAX 请求无法发送。
样例1:
web.dev 域上的网页包含此 iframe:
<iframe id="iframe" src="https://example.com/some-page.html" alt="Sample iframe"></iframe>
网页的 JavaScript 包含此代码以从嵌入页面中的元素获取文本内容:
const iframe = document.getElementById('iframe');
const message = iframe.contentDocument.getElementById('message').innerText;
是否允许使用此 JavaScript?
不允许。由于 iframe 与主机网页不在同一来源,因此浏览器不允许读取嵌入的页面。
样例2:
web.dev 域上的网页包含以下表单:
<form action="https://example.com/results.json">
<label for="email">Enter your email: </label>
<input type="email" name="email" id="email" required>
<button type="submit">Subscribe</button>
</form>
这个表格可以提交吗?
是的。表单数据可以写入元素action属性中指定的跨域 URL <form>。
样例3:
web.dev 域上的网页包含此 iframe:
<iframe src="https://example.com/some-page.html" alt="Sample iframe"></iframe>
是否允许嵌入 iframe?
通常可以。只要源所有者未将 X-Frame-Options HTTP 标头设置为deny或sameorigin,就允许跨源 iframe 嵌入。
扩展:
X-Frame-Options: deny // 不允许在 ifame 中展示,即使是在相同的域名下也不允许
X-Frame-Options: sameorigin // 该页面可以在相同的域名页面的 iframe 中展示
X-Frame-Options: allow-from uri // 该页面允许在指定的来源的 iframe 中展示
样例4:
web.dev 域上的网页包含此画布:
<canvas id="bargraph"></canvas>
网页的 JavaScript 包含以下代码以在画布上绘制图像:
var context = document.getElementById('bargraph').getContext('2d');
var img = new Image();
img.onload = function() {
context.drawImage(img, 0, 0);
};
img.src = 'https://example.com/graph-axes.svg';
这个图像可以画在画布上吗?
这取决于不同的情况。 图像来自不同的来源。如果源的所有者为图像提供了适当的 CORS 标头,则可以安全地绘制图像。否则,图像将 导致错误。
源的更改
在满足某些条件下,页面可以修改它的源。可以将 document.domain 设置为其当前域或者当前域的父域。这一般用于跨域。例如:假设 http://store.company.com/dir/other.html 文档中的一个脚本执行 document.domain = 'company.com',页面将会成功的通过与 http://company.com/dir/page.html 的同源检测(假设 http://company.com/dir/page.html 将其 document.domain 设置为 'company.com')。
端口号是由浏览器另行检查的,对任何 document.domain 的赋值操作,包括 document.domain = document.domain 都会导致端口号被重写为 null。因此,company.com:8080 不能仅通过设置 document.domain = "company.com" 来与company.com 通信。必须在他们双方中都进行赋值,以确保端口号都为 null 。
注意:使用
document.domain来允许子域安全访问其父域时,您需要在父域和子域中设置document.domain为相同的值。这是必要的,即使这样做只是将父域设置回其原始值。不这样做可能会导致权限错误。
每一个窗口(当前页面或者内嵌 iframe)都有自己的 window 和 document 对象(document 可以看做 window 的子属性),我们可以用 window.frames[] 获取 iframe。当我们设置 window.location.host 或者 document.location.host 时,浏览器会跳转页面到相应的主机,但是当我们设置 document.domain 时,浏览器不会跳转页面。
跨源资源共享(CORS)
浏览器的同源策略会阻止读取不同源的资源,它是为了阻止恶意站点读取另一个站点的数据,但是它也会阻止合法使用。启用 CORS 可以让服务器告诉浏览器它允许其他来源访问它的资源。该标准仅适用于浏览器发出的跨源 XHR、media、script、stylesheet、WebGL texture HTTP 请求。
CORS 通知浏览器哪些源可以访问请求的响应数据,是否应在请求中发送 Cookie,以及可以自定义哪些请求头,和使用哪些请求方法。
跨源资源共享标准新增了一组 HTTP 请求头,允许服务器声明哪些源站通过浏览器可以访问哪些资源。另外,对于一些可能对服务器数据产生副作用的请求,浏览器必须先使用 option 方法发起一个预检请求(preflight request),服务器确认之后,才发起实际的 HTTP 请求。
简单请求
简单请求不会触发预检请求,满足以下所有条件可视为简单请求:
-
使用以下请求方法之一:
- GET
- HEAD
- POST
-
允许人为设置的请求头字段为:
- Accept
- Accept-Language
- Content-Language
- Content-Type(有额外的限制)
-
Content-Type 的值限于下面三者之一:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
预检请求
使用 OPTION 方法发起预检请求到服务器,以获知服务器是否允许该实际请求。
例如发送一个预检请求:
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
请求头:
- 请求方法:OPTION。
Access-Control-Request-Method:代表实际请求的请求方法。Access-Control-Request-Header:代表实际请求中添加的自定义请求头信息。
响应头:
Access-Control-Allow-Origin:表示允许访问该资源的origin(源)。Access-Control-Allow-Methods:代表服务器允许使用的请求方法。Access-Control-Allow-Headers:表示服务器允许发送实际请求时自定义的请求头。Access-Control-Max-Age:表示该预检请求的结果的生效时间。在这个时间内,浏览器不用再为同一个实际请求发送预检请求。Access-Control-Expose-Headers:在跨源访问时,XMLHttpRequest对象的getResponseHeader()只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他响应头,则需要服务器通过Access-Control-Expose-Headers来设置,它会将允许浏览器访问的头放入白名单中。
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
这样浏览器就能通过 getResponseHeader() 访问X-My-Custom-Header和 X-Another-Custom-Header 响应头了。
附带身份凭证的请求
一般来说,对于跨源 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送身份凭据,需要设置 XMLHttpRequest 某个特殊的标志位。
- 将
XMLHttpRequest请求的withCredentials标志设置为 true,或者fetch请求的credentials设置为include。 - 服务端的响应头里面如果没有
Access-Control-Allow-Credentials: true,浏览器是不会把响应的内容返回给请求的发送者。 - 对于携带身份凭证的请求,服务器不得设置
Access-Control-Allow-Origin: *,必须设置为指定的origin。
GET /resources/access-control-with-credentials/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
这里的简单请求,即使请求头中指定了 Cookie,但是如果响应头里面没有 Access-Control-Allow-Credentials: true,响应的内容也不会返回给请求的发起者。
CORS 是如何工作的
- 当浏览器发出跨域请求时,浏览器会自动给请求头加上一个 origin,来表示当前的源(协议、域名、端口)。
- 在服务器端,当它看到 origin 字段时,如果允许访问,它需要给响应头加上一个
Access-Control-Allow-Origin,并设定值为*或者当前请求源。 - 当浏览器接收到包含
Access-Control-Allow-Origin的响应头时,浏览器接收该响应。