浏览器安全

一、Web页面安全

1. 为什么Web页面需要安全策略?

因为如果在没有安全保障的Web世界中,我们是没有隐私的,所以需要安全策略来保障我们的隐私和数据的安全。

比如说,如果Web页面没有安全策略,那么当你打开一个银行站点,然后又一不小心打开了一个恶意的站点,那么恶意站点可以做许多事情:

  • 修改银行站点的DOM,CSSOM等信息
  • 在银行站点内部插入JavaScript脚本
  • 劫持用户登录的用户名和密码
  • 读取银行站点的Cookie、LocalStorage和IndexedDB等数据
  • 将这些信息上传至自己的服务器,这样就可以在你不知情的情况下伪造一些转账请求信息

这就引出了Web页面中最基本、最核心的安全策略——同源策略(Same-origin policy)。

2. 同源策略(Same-origin policy)

同源:如果两个URL的协议域名端⼝号都相同,我们就称这两个URL同源。

同源策略:浏览器默认两个相同的源之间是可以相互访问资源和操作DOM的。两个不同的源之间若想要相互访问资源或者操作DOM,那么会有一套基础的安全策略的制约,这个安全策略就是同源策略。

同源策略主要表现在DOMWeb数据网络三个层面。

  1. DOM层面

    同源策略限制了来自不同源的JS脚本对当前DOM对象读和写的操作。例如:

    • 从一个页面打开同源的另一个页面,第二个页面可以通过opener.document来获取同源页面(第一个页面)的DOM(opener:返回打开当前窗口的那个窗口的引用,也就是指向第一个页面的window对象)
    • 从一个页面打开不同源的另一个页面,第二个页面是无法操作第一个页面的DOM
  2. 数据层面

    同源策略限制了不同源的站点读取当前站点的Cookie、LocalStorage、IndexedDB等数据。

    同源的站点之间可以访问公共的数据,不同源之间是无法访问的。

  3. 网络层面

    同源策略限制了通过XMLHttpRequest、Fetch等方式将站点的数据发送给不同源的站点。

3. 浏览器出让的安全性

安全性和便利性是相对的,让不同的源之间完全隔离,毫无疑问是绝对安全的措施,但这也使得Web项目难以开发和使用。所以浏览器出让了一些安全性来满足便利性,使二者之间能有个权衡。

(1) 页面中可以嵌入第三方资源

由于同源策略的限制,要让一个页面的资源全部都来自于一个源,也就是要将所有HTML、CSS、JS、图片等资源都部署到一台服务器上,那么就无法将一些资源部署到CDN上,所以浏览器为页面中引用资源开了一个"口子",让其引用外部文件。

比如说,我们可以通过<img>href属性来获取图片资源,通过<script><link>src属性来获取JS和CSS资源。

但是,这也带来了很多危害,比如说HTML被恶意程序劫持,同时向其中插入了恶意的JS脚本。当页面被加载时,恶意的JS脚本运行,可能会将页面的敏感数据,如Cookie、LocalStorage、IndexedDB通过XSS的手段发送给服务器。

为了解决XSS攻击,浏览器引入了内容安全策略(Content Security Policy,CSP)

CSP的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联的JS代码,以此来减少XSS攻击。

JSONP跨域
  1. 添加一个<script>元素,请求的URL携带一个callback查询参数(?callback=bar)
  2. 服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}))
  3. 客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function jsonp({ url, params, cb }) {
return new Promise((resolve, reject) => {
const script = document.createElement("script")
window[cb] = function (data) {
resolve(data)
document.body.removeChild(script)
}
params = { ...params, cb }
const arr = []
Reflect.ownKeys(params).forEach(key => arr.push(`${key}=${params[key]}`))
script.src = `${url}?${arr.join("&")}`
document.body.appendChild(script)
})
}

// https://www.example.com/sug?prod=pc&wd=ab&cb=callback
jsonp({
url: "https://www.example.com/sug",
params: {
prod: "pc",
wd: "ab"
},
cb: "callback"
}).then(data => {
console.log(data)
})
1
2
3
4
5
6
7
8
9
const express = require("express")
const app = express()

app.get("/sug", function (req, res) {
let { cb } = req.query
res.send(`${cb}("data")`)
})

app.listen(3000)

(2) 跨域资源共享(CORS)

由于同源策略的限制,两个不同源的站点之间是无法使用XMLHttpRequest和Fetch发送请求的,而在实际应用中,这会大大制约我们的生产力,为了解决这个问题,浏览器引入了**跨域资源共享(Cross Origin Resources Sharing,CORS)**机制,使用CORS机制可以进行跨域访问控制,从而使跨域传输数据得以安全进行。

CORS机制的工作原理

CORS把HTTP请求分为两种请求,一种是简单请求,另一种是非简单请求

① 简单请求

简单请求也就是简单的请求方法,简单的请求头信息。

  • 请求方法是:GET、POST、HEAD之一
  • 请求头字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type:text/plain、multipart/form-data、application/x-www-form-urlencoded 之一

对于简单请求,浏览器直接发出CORS请求。

浏览器在发送请求的时候会自动加上Origin头字段,表明本次请求来自哪个域。

服务器在响应请求时,如果未携带响应头Access-Control-Allow-Origin字段,则浏览器就知道出错了。

② 非简单请求

非简单请求也就是特殊的请求方法,特殊的请求头信息。

1. 预检请求

在正式发起请求之前,浏览器会发送一个OPTIONS预检请求

预检请求中同时携带了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers头字段,告知服务器正式请求的域名、请求方法和请求头字段。

服务器依据这些字段来决定正式请求是否被允许,然后响应这个预检请求,同时携带响应头Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccss-Control-Allow-Headers字段,告知浏览器正式请求可以使用的域名、请求方法和请求头字段。

当响应携带Access-Control-Max-Age头字段时,告诉浏览器这个预检请求的有效期为多少秒,也就是说下次同样的请求,如果在有效期内,浏览器就不需要再发起预检请求。

2. 正式请求

一旦服务器通过了预检请求之后,浏览器就可以发送正式请求,就跟简单请求一样。

(3) 跨文档消息机制

由于同源策略的限制,两个不同源的页面是无法互相操作DOM的,而在实际应用中,可能需要两个不同源的DOM之间进行通信,所以浏览器又引入了跨文档消息机制,可以通过window.postMessage的API来和不同源的DOM之间进行比较安全的通信。

2. XSS攻击

(1) 什么是XSS

XSS(Cross-site Scripting),跨站脚本,为了与“CSS”区分开,故简称XSS。

XSS攻击指攻击者往HTML文件或者DOM中注入恶意脚本,从而使用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

(2) XSS的危害

当HTML被注入了恶意的JS脚本时,浏览器是无法区分这些脚本是恶意的,所以恶意的脚本也拥有了其他脚本的权限。比如:

  • 窃取Cookie

    恶意JS脚本可以通过document.cookie获取cookie信息,然后通过XMLHttpRequest或Fetch加上CORS将数据发送给恶意服务器。恶意服务器拿到用户的Cookie信息后,就可以在其他电脑上模拟用户的登录,然后进行一些恶意操作。

  • 监听用户行为

    恶意JS脚本可以通过addEventListener来监听键盘事件,比如获取用户输入的隐私信息,将其发送给恶意服务器。恶意服务器拿到用户输入的隐私信息后,可以进行一些恶意操作。

  • 修改DOM

    恶意JS脚本可以修改DOM来伪造假的登录窗口,用来窃取用户输入的用户名和密码等隐私信息。

    同时还有可能添加浮窗广告,严重影响用户体验。

总之,如果让页面插入了恶意脚本,那么就相当于把页面的隐私数据和行为完全暴露给了外界。

(3) 恶意脚本注入

通常情况下,XSS攻击主要分为存储型XSS攻击反射型XSS攻击基于DOM的XSS攻击三种方式注入恶意脚本。

① 存储型XSS攻击
  1. 首先,攻击者利用站点漏洞将一段恶意JS代码提交到保存网站数据的数据库中
  2. 然后,用户访问了包含恶意JS代码的页面
  3. 最后,用户浏览该页面的时候,恶意JS代码运行,将用户的隐私数据上传到恶意服务器
② 反射型XSS攻击
  1. 首先,攻击者将一段还有恶意代码的请求提交给Web服务器
  2. 然后,Web服务器接收到请求后,又将恶意代码反射给了浏览器端

反射型和存储型的不同之处在于,Web服务器不会存储反射型的恶意脚本

③ 基于DOM的XSS攻击

基于DOM的XSS攻击是不牵涉到Web服务器的。攻击者通过各种手段将恶意脚本注入页面,比如通过网络劫持在页面传输过程中修改HTML的内容

(4) XSS预防

无论是何种类型的XSS攻击,他们的共同点都是往浏览器中的页面注入恶意脚本,然后通过恶意脚本将用户信息发送至恶意服务器。所以,要阻止XSS攻击,可以通过阻止恶意JS脚本的注入和恶意消息的发送来实现。

① 过滤或转码

不管是存储型还是反射型XSS攻击,我们都可以在服务端将一些关键字符进行转码或过滤。

比如:

  • <script>...</script>整个都给过滤掉
  • <>转码为&lt;&gt;
② 充分利用CSP

仅仅只是依靠服务端执行过滤和转码是不够的,我们还需要把CSP策略充分利用进来,以此来大大减少XSS危险。

CSP大致功能:

  • 禁止像第三方域提交数据,这样可以防止用户数据泄露
  • 禁止执行内联脚本和未授权的脚本
  • 限制加载其他域下的资源文件,这样即使被注入了恶意脚本,脚本也是不会被加载的
  • 提供上报XSS机制,可以帮助我们尽快发现有哪些XSS攻击
③ 使用HttpOnly属性

很多XSS攻击都会盗用Cookie,所以我们还可以通过HttpOnly属性来保护Cookie的安全。

服务器在设置set-cookie头字段时,可以用HttpOnly属性来标记这个字段,表名该cookie只能在http请求过程中使用,无法通过JS脚本获取到该cookie。

3. CSRF攻击

(1) 什么是CSRF以及危害

CSRF(Cross-site request forgery),跨站请求伪造。

CSRF攻击指攻击者引诱用户打开攻击者部署的网站,在攻击者的网站中,利用用户的登录状态发起跨站请求。

攻击者可以利用用户的登录状态来做一些转账等恶意操作。

(2) CSRF攻击方式

CSRF攻击的三个必要条件:

  1. 目标站点必须有CSRF漏洞
  2. 用户登录过目标站点,并且在浏览器上保持着该站点的登录状态
  3. 需要用户打开另一个第三方站点

比如说:当用户登录并访问了https://example.com,在没有登出的情况下,打开了恶意的网站后,攻击者通常有3种方式来实时CSRF攻击:

① 自动发起get请求
1
2
3
4
5
<html> 
<body>
<img src="https://example.com/sendcoin?user=hacker&number=100">
</body>
</html>

当该页面加载时,浏览器认为这是一张图片,会对这个链接发起get请求,如果服务器没有对请求做判断的话,那么服务器就会认为这是一个正常的请求,执行转账操作。

② 自动发起post请求

有些接口可能使用的post请求,但是攻击者同样可以利用form表单发起post请求。

1
2
3
4
5
6
7
8
9
<html> 
<body>
<form id='hacker-form' action="https://time.geekbang.org/sendcoin" method=POST>
<input type="hidden" name="user" value="hacker" />
<input type="hidden" name="number" value="100" />
</form>
<script> document.getElementById('hacker-form').submit(); </script>
</body>
</html>

当页面加载时,浏览器会自动触发这个隐藏表单域的提交事件,同样的,如果服务器没有对该请求做判断的话,那么服务器也会认为这是一个正常请求,执行转账操作。

③ 引诱用户点击链接

除了用户自己去浏览恶意网站,我们还可以通过诱导的方式,比如邮件或者论坛等方式,诱导用户点击恶意的链接,跳转到恶意的网站,执行转账等恶意操作。

1
2
3
4
5
6
7
8
<div> 
<img width=150 src=http://images.xxx.cn/xxx.jpg>
</div>
<div>
<a href="https://example.com/sendcoin?user=hacker&number=100">
点击下载美⼥照⽚
</a>
</div>

(3) CSRF与XSS的区别

CSRF与XSS攻击相比,并不需要向页面注入恶意代码,仅仅只需要利用服务器的漏洞和用户的登录状态来实时攻击,

所以CSRF攻击是无法获取用户页面的数据的。

(4) CSRF预防

通过CSRF攻击的3个条件,我们能发现一个关。键点就是目标站点的服务器有漏洞,所以对于CSRF攻击,我们主要的防范手段就是提升服务器的安全性。

① 利用Cookie的SameSite属性

Cookie是浏览器和服务器之间维护用户登录状态的一个关键数据,要防止CSRF攻击,我们最好可以实现从第三方站点发送请求时禁止携带Cookie。

服务器在响应头中添加set-cookie头字段时,给这个cookie设置SameSite属性,SameSite属性有3个值

  • Strict:严格,完全禁止第三方站点请求时携带该Cookie
  • Lax:较为宽松,只有在第三方站点使用像<a>链接和GET表单时可以携带Cookie
  • None:任何情况下都可以携带

通过Cookie的SameSite属性,就可以很好的解决这个问题。可以将一些关键的Cookie设置为Strict或者Lax,这样在跨站请求的时候,这些Cookie就不会被携带了。

② 验证请求的来源站点

由于CSRF攻击大多来自于第三方站点,因此服务器可以完全拒绝第三方站点的请求。

通过HTTP请求头中的RefererOrigin字段可以判断请求的来源。

  • Referer:记录了HTTP请求的来源地址。有些场景并不适合将请求来源的URL暴露给服务器,因此提供了一个Referrer Policy选项来管理Referrer。
  • Origin:记录了HTTP请求的来源域名。由于Referer并不是太可靠,所有有了另一个可靠的属性,那就是Origin,Origin只包含了域名信息,并不会携带后面的URL路径信息,这也是和Referrer的一个主要区别。在一些场合下,比如XMLHttpRequest、Fetch发起请求时,会自动带上Origin这个头字段。

所以,通常,服务器的优先策略是优先判断Origin,如果没有Origin属性,在根据实际情况判断是否需要使用Referer。

③ CSRF Token验证

CSRF Token验证大致分为2个步骤:

  1. 浏览器向服务器发送请求时,服务器生成一个CSRF Token(一个复杂的字符串)返回给浏览器。
  2. 浏览器在发起一些重要的请求时,需要带上这个CSRF Token,然后服务器会验证这个Token是否合法,。如果是从第三方站点发出的请求,那么是无法获取到CSRF Token的,所以即使第三方发起了请求,也会因为没有CSRF Token而拒绝这个请求的。

二、浏览器网络安全

// TODO

三、浏览器系统安全

// TODO

参考链接