同源策略
同源策略是一种关键的安全机制,它限制了一个 源 加载的文档或脚本与另一个源的资源进行交互的方式。
它有助于隔离潜在的恶意文档,减少可能的攻击向量。例如,它阻止互联网上的恶意网站通过浏览器中的 JS 读取第三方 Web 邮件服务(用户已登录)或公司内网(攻击者无法直接访问,因为它没有公共 IP 地址)的数据,并将该数据中继给攻击者。
源的定义
如果两个 URL 的 协议、端口(如果指定)和 主机 都相同,则它们具有相同的源。您可能会看到这被称为“方案/主机/端口元组”或简称为“元组”。(“元组”是一组共同构成整体的项——这是双/三/四/五/等……的通用形式。)
下表给出了与 URL http://store.company.com/dir/page.html 进行源比较的示例
| URL | 结果 | 原因 |
|---|---|---|
http://store.company.com/dir2/other.html |
相同源 | 只有路径不同 |
http://store.company.com/dir/inner/another.html |
相同源 | 只有路径不同 |
https://store.company.com/page.html |
失败 | 协议不同 |
http://store.company.com:81/dir/page.html |
失败 | 端口不同(http:// 默认端口为 80) |
http://news.company.com/dir/page.html |
失败 | 主机不同 |
继承的源
从具有 about:blank 或 javascript: URL 的页面执行的脚本会继承包含该 URL 的文档的源,因为这些类型的 URL 不包含有关源服务器的信息。
例如,about:blank 通常用作新的、空白的弹出窗口的 URL,父脚本会将内容写入其中(例如,通过 Window.open() 机制)。如果此弹出窗口也包含 JavaScript,则该脚本将继承与其创建者相同的源。
data: URL 会获得一个新的、空的、安全上下文。
文件源
现代浏览器通常将使用 file:/// 方案加载的文件的源视为不透明源。这意味着,如果一个文件包含同一文件夹中的其他文件(例如),它们不会被假定来自同一源,并可能触发 CORS 错误。
请注意,URL 规范指出文件的源是实现定义的,并且某些浏览器可能会将同一目录或子目录中的文件视为同源,尽管这会产生安全隐患。
更改源
警告:此处描述的方法(使用 document.domain 设置器)已弃用,因为它会削弱同源策略提供的安全保护,并使浏览器中的源模型复杂化,导致互操作性问题和安全漏洞。
页面可以更改其自身的源,但有一些限制。脚本可以将 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 以了解更多信息)。但是,company.com不能将 document.domain 设置为 othercompany.com,因为它不是 company.com 的超域。
端口号由浏览器单独检查。任何对 document.domain 的调用,包括 document.domain = document.domain,都会导致端口号被覆盖为 null。因此,仅在第一个中设置 document.domain = "company.com" 不能使 company.com:8080 与 company.com 通信。必须同时在两者中设置,以便它们的端口号都为 null。
该机制存在一些限制。例如,如果文档位于沙盒化的 <iframe> 中,它将抛出 SecurityError DOMException,并且以这种方式更改源不会影响许多 Web API(例如 localStorage、indexedDB、BroadcastChannel、SharedWorker)使用的源检查。更全面的故障案例列表可以在 Document.domain > 故障 中找到。
注意:当使用 document.domain 允许子域访问其父域时,您需要在父域和子域中都将 document.domain 设置为相同的值。即使这样做是为了将父域设置回其原始值,也是必需的。否则可能会导致权限错误。
跨域网络访问
同源策略控制不同源之间的交互,例如当您使用 fetch() 或 <img> 元素时。这些交互通常分为三类
- 跨域写入通常是允许的。例如链接、重定向和表单提交。某些 HTTP 请求需要 预检。
- 跨域嵌入通常是允许的。(下面列出了示例。)
- 跨域读取通常是不允许的,但读取访问经常通过嵌入泄露。例如,您可以读取嵌入图像的尺寸、嵌入脚本的操作或嵌入资源的可用性。
以下是一些可能被跨域嵌入的资源示例
- 带有
<script src="…"></script>的 JavaScript。语法错误的错误详细信息仅对同源脚本可用。 - 使用
<link rel="stylesheet" href="…">应用的 CSS。由于 CSS 的宽松语法规则,跨域 CSS 需要正确的Content-Type标头。如果是一个 MIME 类型不正确且资源不以有效 CSS 构造开头的跨域加载,浏览器会阻止样式表加载。 <img>显示的图像。<video>和<audio>播放的媒体。- 使用
<object>和<embed>嵌入的外部资源。 @font-face应用的字体。某些浏览器允许跨域字体,其他浏览器则要求同源。- 由
<iframe>嵌入的任何内容。站点可以使用X-Frame-Options标头来防止跨域帧。
如何允许跨域访问
如何阻止跨域访问
- 为了防止跨域写入,请在请求中检查一个不可猜测的令牌——称为跨站请求伪造 (CSRF) 令牌。您必须阻止对需要此令牌的页面的跨域读取。
- 为了阻止对资源的跨域读取,请确保它无法被嵌入。通常需要阻止嵌入,因为嵌入资源总是会泄露有关该资源的一些信息。
- 为了阻止跨域嵌入,请确保您的资源不能被解释为上面列出的可嵌入格式之一。浏览器可能不会尊重
Content-Type标头。例如,如果您将<script>标签指向一个 HTML 文档,浏览器将尝试将 HTML 解析为 JavaScript。当您的资源不是您网站的入口点时,您还可以使用 CSRF 令牌来防止嵌入。
跨域脚本 API 访问
JavaScript API,如 iframe.contentWindow、window.parent、window.open 和 window.opener 允许文档直接相互引用。当两个文档不具有相同的源时,这些引用对 Window 和 Location 对象的访问非常有限,如下一个两个部分所述。
要实现不同源之间的文档通信,请使用 window.postMessage。
Window
以下对这些 Window 属性的跨域访问是允许的
| 属性 | |
|---|---|
window.closed |
只读。 |
window.frames |
只读。 |
window.length |
只读。 |
window.location |
读/写。 |
window.opener |
只读。 |
window.parent |
只读。 |
window.self |
只读。 |
window.top |
只读。 |
window.window |
只读。 |
某些浏览器允许访问比上述更多的属性。
Location
以下对 Location 属性的跨域访问是允许的
| 属性 | |
|---|---|
location.href |
只写。 |
某些浏览器允许访问比上述更多的属性。
跨域数据存储访问
对浏览器中存储的数据(如 Web Storage 和 IndexedDB)的访问按源进行分隔。每个源都有自己的独立存储,一个源中的 JavaScript 无法读取或写入属于另一个源的存储。
Cookie 使用独立的源定义。页面可以为其自己的域或任何父域设置 cookie,只要父域不是公共后缀。Firefox 和 Chrome 使用 公共后缀列表来确定域是否为公共后缀。设置 cookie 时,您可以使用 Domain、Path、Secure 和 HttpOnly 标志来限制其可用性。读取 cookie 时,您看不到它是从哪里设置的。即使您只使用安全的 https 连接,您看到的任何 cookie 都可能是在不安全的连接上设置的。