本文目录

[[toc]]

XSS

什么是 XSS

Cross-Site Scripting (跨站脚本攻击)简称 XSS ,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie 、 SessionID 等,进而危害数据安全

存储型 XSS

攻击者将恶意代码提交到目标网站的数据库中。

用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。

用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。

恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS

攻击者构造出特殊的 URL,其中包含恶意代码。

用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。

用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。

恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

DOM 型 XSS

攻击者构造出特殊的 URL,其中包含恶意代码。

用户打开带有恶意代码的 URL。

用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。

恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

XSS 防范措施

CSP ( Content Security Policy )

禁止加载外域代码,防止复杂的攻击逻辑

禁止外域提交,网站被攻击后,用户的数据不会泄露到外域

禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)

禁止未授权的脚本执行(新特性,Google Map 移动版在使用)

合理使用上报可以及时发现 XSS,利于尽快修复问题

输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度

其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

CSRF

什么是 CSRF

CSRF ( Cross-site request forgery ) 跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的

典型过程

受害者登录 a.com ,并保留了登录凭证( Cookie )

攻击者引诱受害者访问了 b.com

b.com 向 a.com 发送了一个请求: a.com/act=xx 浏览器会默认携带 a.com 的 Cookie

a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求

a.com 以受害者的名义执行了 act=xx

攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让 a.com 执行了自己定义的操作

CSRF 的特点

攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生

攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据

整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”

跨站请求可以用各种方式:图片 URL、超链接、CORS、Form 提交等等部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪

总结如下:

  • CSRF (通常)发生在第三方域名。
  • CSRF 攻击者不能获取到 Cookie 等信息,只是使用。

CSRF 防护策略

整体思路:

  • 阻止不明外域的访问
    • 同源检测
    • Samesite Cookie
  • 提交时要求附加本域才能获取的信息
    • CSRF Token
    • 双重 Cookie 验证
    • 敏感操作二次认证

同源检测

  • Referer Header 检查: 验证请求头中的 Referer 字段是否来自合法域名,阻止跨域请求。
  • Origin Header 验证: 检查 Origin 字段(适用于AJAX请求),确保请求源与目标站点一致。

Samesite Cookie 属性

从源头上解决这个问题,Google 起草了一份草案来改进 HTTP 协议,那就是为 Set-Cookie 响应头新增 Samesite 属性,它用来标明这个 Cookie 是个“同站 Cookie”,同站 Cookie 只能作为第一方 Cookie ,不能作为第三方 Cookie , Samesite 有两个属性值,分别是 StrictLax

  • Samesite=Strict: 这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外
  • Samesite=Lax: 这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个 GET 请求,则这个 Cookie 可以作为第三方 Cookie

CSRF Token

原理: 服务器为每个用户会话生成唯一的随机令牌( Token ),嵌入表单或请求头中。服务器验证请求时检查 Token 有效性,非法请求因缺乏正确 Token 被拒绝

CSRF Token 的防护策略分为三个步骤:

  1. 将 CSRF Token 从服务端传递到浏览器端( 网页注入 )
  2. 页面提交的请求携带这个 Token
  3. 服务器验证 Token 是否正确

双重 Cookie 验证

双重 Cookie 采用以下流程:

  1. 在用户访问网站页面时,向请求域名注入一个 Cookie ,内容为随机字符串(例如 csrfcookie=v8g9e4ksfhw )。
  2. 在前端向后端发起请求时,取出 Cookie ,并添加到 URL 的参数中(如: https://www.a.com/comment?csrfcookie=v8g9e4ksfhw )。
  3. 后端接口验证 Cookie 中的字段与 URL 参数中的字段是否一致,不一致则拒绝。

二次认证

在进行敏感操作时,通过密码 、 TOTP 、生物信息等方式进行二次认证

参考文献

浏览器同源策略

是什么

Same-origin policy 它的含义是指, A 网页设置的 Cookie , B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是“三个相同”: 协议相同、域名相同、端口相同

解决了什么问题?

为了保证用户信息的安全,防止恶意的网站窃取数据,是浏览器安全的基石

设想这样一种情况: A 网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取 A 网站的 Cookie,会发生什么?

很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

由此可见,“同源策略”是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了

规避方式

Cookie

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain 共享 Cookie。

举例来说, A 网页是 http://w1.example.com/a.html , B 网页是 http://w2.example.com/b.html ,那么只要设置相同的 document.domain ,两个网页就可以共享 Cookie

注意,这种方法只适用于 Cookie 和 iframe 窗口, LocalStorage 和 IndexDB 无法通过这种方法;另外,服务器也可以在设置 Cookie 的时候,指定 Cookie 的所属域名为一级域名,比如 .example.com

iframe

如果两个窗口一级域名相同,只是二级域名不同,那么设置 document.domain 属性,就可以规避同源政策

对于完全不同源的网站,目前有三种方法,可以解决跨域窗口的通信问题

片段标识符( fragment identifier )

URL 的#号后面的部分,比如 http://example.com/x.html#fragment#fragment 。如果只是改变片段标识符,页面不会重新刷新

// 父窗口可以把信息,写入子窗口的片段标识符
const src = `${originURL}#${data}`
document.getElementById('myIFrame').src = src

// 子窗口通过监听hashchange事件得到通知。
window.onhashchange = checkMessage

function checkMessage() {
  const message = window.location.hash
  // ...
}

// 子窗口也可以改变父窗口的片段标识符
parent.location.href = `${target}#${hash}`

window.name

浏览器窗口有 window.name 属性。这个属性的最大特点是,无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页可以读取它

  • 优点: window.name 容量很大,可以放置非常长的字符串;
  • 缺点: 必须监听子窗口 window.name 属性的变化,影响网页性能。

window.postMessage

HTML5 为了解决这个问题,引入了一个全新的 API :跨文档通信 API;

这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源

// message事件的事件对象event,提供以下三个属性
// event.source:发送消息的窗口
// event.origin: 消息发向的网址
// event.data: 消息内容

// 子窗口通过event.source属性引用父窗口,然后发送消息
window.addEventListener('message', receiveMessage)
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*')
}

LocalStorage

window.postMessage,读写其他窗口的 LocalStorage 也成为了可能

// 父窗口发送消息
const win = document.getElementsByTagName('iframe')[0].contentWindow
const obj = { name: 'Jack' }
win.postMessage(
  JSON.stringify({ key: 'storage', data: obj }),
  'http://bbb.com'
)

// 子窗口将父窗口发来的消息,写入自己的LocalStorage
window.onmessage = function (e) {
  if (e.origin !== 'http://bbb.com') {
    return
  }
  const payload = JSON.parse(e.data)
  localStorage.setItem(payload.key, JSON.stringify(payload.data))
}

JSONP

  • 基本思想是,通过添加一个 <script> 元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来
function addScriptTag(src) {
  const script = document.createElement('script')
  script.setAttribute('type', 'text/javascript')
  script.src = src
  document.body.appendChild(script)
}

window.onload = function () {
  // 这里依赖后端,需要响应的 JS 代码中,显式调用 Callback 函数
  // Script 在加载后会被立即执行,这样才能实现完整的 JSONP
  addScriptTag('http://example.com/ip?callback=foo')
}

function foo(data) {
  console.log(`Your public IP address is: ${data.ip}`)
}

WebSocket

WebSocket 是一种通信协议,使用 ws:// (非加密)和 wss:// (加密)作为协议前缀。 该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信

浏览器发出的 WebSocket 请求的头信息

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

有一个字段是 Origin ,表示该请求的请求源( origin ),即发自哪个域名。正是因为有了 Origin 这个字段,所以 WebSocket 才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信

CORS

  • CORS 需要浏览器和服务器同时支持。 目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10 。
  • 整个 CORS 通信过程,都是 浏览器自动完成,不需要用户参与。
  • 对于开发者来说, CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。
  • 浏览器一旦发现 AJAX 请求跨源,就会 自动添加一些附加的头信息 ,有时还会多出一次附加的请求,但用户不会有感觉。
  • 实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信

简单请求 和 非简单请求

浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request);浏览器对这两种请求的处理,是不一样的

只要同时满足以下两大条件,就属于简单请求

  1. 请求方法是以下三种方法之一: HEADGETPOST

  2. HTTP 的头信息不超出以下几种字段

HTTP 的头信息不超出以下几种字段允许的头信息字段及其内容限制
Accept任意值,表示客户端可接受的内容类型范围
Accept-Language任意值,表示客户端优先的语言区域设置
Content-Language任意值,表示请求体中的内容所使用的语言
Last-Event-ID任意值,用于服务器向客户端发送事件流时识别事件序列
Content-Type只限于以下三个值:
1. application/x-www-form-urlencoded
2. multipart/form-data
3. text/plain

注:以上表格中,“任意值”表示这些字段可以携带用户自定义的内容,但必须在 CORS 跨域资源共享策略允许范围内。

简单请求基本流程

---
title: 简单请求访问时序图
---
sequenceDiagram
  participant Browser
  participant Server

  Browser->>Server: GET/POST/HEAD 请求<br/>携带 Origin 头(如 Origin: https://example.com)
  Note over Server: 检查 Origin 和配置
  Server-->>Browser: 响应数据<br/>携带 Access-Control-Allow-Origin: * 或具体源
  Note over Browser: 检查响应头是否匹配 Origin<br/>若匹配则允许访问数据
HTTP Header(响应头)描述
Access-Control-Allow-Origin必须的。它的值要么是请求时 Origin 字段的值,要么是一个 * ,表示接受任意域名的请求求。
Access-Control-Allow-Credentials一个布尔值,表示是否允许发送 Cookie 。默认情况下, Cookie 不包括在 CORS 请求之中。设为 true ,即表示服务器明确许可, Cookie 可以包含在请求中,一起发给服务器。这个值也只能设为 true ,如果服务器不要浏览器发送 Cookie ,删除该字段即可
Access-Control-Expose-Headers请求时, XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到 6 个基本字段: Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma 。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。

withCredentials 属性

CORS 请求默认不发送 Cookie 和 HTTP 认证信息。如果要把 Cookie 发到服务器,一方面要服务器同意,指定 Access-Control-Allow-Credentials 字段;

另一方面,开发者必须在 AJAX 请求中打开 withCredentials 属性。

否则,即使服务器同意发送 Cookie ,浏览器也不会发送。或者,服务器要求设置 Cookie ,浏览器也不会处理

但是,如果 省略 withCredentials 设置,有的浏览器还是会一起发送 Cookie 。这时,可以显式关闭 withCredentials

需要注意的是,如果要发送 Cookie , Access-Control-Allow-Origin 就不能设为星号 ,必须指定明确的、 与请求网页一致的域名

const xhr = new XMLHttpRequest()
xhr.withCredentials = true

非简单请求流程

---
title: 非简单请求访问时序图
---
sequenceDiagram
  participant Browser
  participant Server

  Browser->>Server: OPTIONS 预检请求<br/>携带 Origin、Access-Control-Request-Method、Access-Control-Request-Headers
  Note over Server: 检查 Origin、允许的方法和请求头
  Server-->>Browser: 预检响应<br/>携带 Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers 等
  Note over Browser: 验证预检响应是否通过
  Browser->>Server: 发送实际请求(如 PUT/DELETE)
  Server-->>Browser: 响应实际数据<br/>携带 Access-Control-Allow-Origin

预检请求

request

“预检”请求用的请求方法是 OPTIONS ,表示这个请求是用来询问的。头信息里面,关键字段是 Origin ,表示请求来自哪个源

  • Access-Control-Request-Method 字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法
  • Access-Control-Request-Headers 字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
response

服务器收到"预检"请求以后,检查了 OriginAccess-Control-Request-MethodAccess-Control-Request-Headers 字段以后,确认允许跨源请求,就可以做出回应

HTTP 响应中 关键的是 Access-Control-Allow-Origin 字段,表示 http://api.bob.com 可以请求数据。该字段也可以设为 * ,表示同意任意跨源请求

如果服务器 否定了“预检”请求,会返回一个 正常的HTTP回应 ,但是 没有任何CORS相关的头信息字段 。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被 XMLHttpRequest 对象的 onerror 回调函数捕获

  • 成功响应
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://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
  • 失败响应
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他 CORS 相关字段如下:

  • Access-Control-Allow-Methods 字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求
  • Access-Control-Allow-Headers 如果浏览器请求包括 Access-Control-Request-Headers 字段,则 Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串, 表明服务器支持的所有头信息字段 ,不限于浏览器在"预检"中请求的字段
  • Access-Control-Allow-Credentials 该字段与简单请求时的含义相同
  • Access-Control-Max-Age 该字段可选,用来 指定本次预检请求的有效期,单位为秒 。上面结果中,有效期是 20 天(1728000 秒),即允许缓存该条回应 1728000 秒(即 20 天),在此期间,不用发出另一条预检请求。
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

一旦服务器通过了"预检"请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段

总结

解决方案

解决方案描述
Cookie通过在请求中携带 Cookie 信息进行跨域通信。
document.domain适用于主域相同,子域不同的情况,通过设置 document.domain 实现跨域通信。
iframe 片段识别符通过在 URL 的片段标识符中传递信息进行跨域通信。
window.name利用 window.name 属性进行跨域通信。
window.postMessage使用 HTML5 中新增的 window.postMessage 方法进行跨域通信。
LocalStorage利用浏览器本地存储进行跨域通信。
JSONP通过动态生成 script 标签,利用 src 属性加载跨域资源,实现跨域通信。
WebSocket使用 WebSocket 协议进行跨域通信,不受同源策略限制。
CORS在服务器端设置响应头,允许指定的域访问资源,从而实现跨域请求。

CORS 关键请求头

请求头字段描述
Origin用来说明本次请求来自哪个源(协议 + 域名 + 端口)。
Access-Control-Request-Method用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。
Access-Control-Request-Headers指定浏览器 CORS 请求会额外发送的头信息字段。

CORS 关键响应头

响应头字段描述
Access-Control-Allow-Origin指定允许访问资源的域名
Access-Control-Allow-Methods指定服务器支持的所有跨域请求的方法
Access-Control-Allow-Headers指定服务器支持的所有头信息字段
Access-Control-Allow-Credentials指定是否允许发送 Cookie 。
Access-Control-Max-Age指定本次预检请求的有效期
Access-Control-Expose-Headers指定浏览器可以访问的响应头字段

Content Security Policy

CSP 是什么?

Content Security Polic (CSP) 的实质就是白名单制度,开发者明确告诉客户端,哪些外部资源可以加载和执行,等同于提供白名单。它的实现和执行全部由浏览器完成,开发者只需提供配置

CSP 解决了什么问题?

跨域脚本攻击 XSS 是最常见、危害最大的网页安全漏洞,了防止它们,要采取很多编程措施,非常麻烦。很多人提出,能不能根本上解决问题,浏览器自动禁止外部注入恶意脚本,这就是“网页安全政策”的来历

CSP 如何启用?

  • 通过 HTTP 头信息的 Content-Security-Policy 的字段
Content-Security-Policy: script-src 'self'; object-src 'none';style-src cdn.example.org third-party.org; child-src https:
  • 通过网页的<meta>标签
<meta
  http-equiv="Content-Security-Policy"
  content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:"
/>

上述配置解释

  • 脚本: 只信任当前域名
  • <object>标签: 不信任任何 URL ,即不加载任何资源
  • 样式表: 只信任 cdn.example.org 和 third-party.org
  • 框架( frame ): 必须使用 HTTPS 协议加载
  • 其他资源: 没有限制

CSP 如何配置?

资源加载限制配置

指令描述
script-src外部脚本
style-src样式表
img-src图像
media-src媒体文件(音频和视频)
font-src字体文件
object-src插件(比如 Flash )
child-src框架
frame-ancestors嵌入的外部资源(比如 <frame><iframe><embed><applet>
connect-srcHTTP 连接(通过 XHR、WebSockets、EventSource 等)
worker-srcworker 脚本
manifest-srcmanifest 文件

default-src

default-src 用来设置上面各个选项的默认值

URL 限制

网页会跟其他 URL 发生联系,这时也可以加以限制

指令描述
frame-ancestors限制嵌入框架的网页
base-uri限制 <base#href>
form-action限制 <form#action>

其他限制

指令描述
block-all-mixed-contentHTTPS 网页不得加载 HTTP 资源(浏览器已经默认开启)
upgrade-insecure-requests自动将网页上所有加载外部资源的 HTTP 链接换成 HTTPS 协议
plugin-types限制可以使用的插件格式
sandbox浏览器行为的限制,比如不能有弹出窗口等

report-uri

report-uri 就用来告诉浏览器,应该把注入行为报告给哪个网址

CSP 如何配置选项值?

每个限制选项可以设置以下几种值,这些值就构成了白名单

指令描述
主机名example.org, https://example.com:443
路径名example.org/resources/js/
通配符.example.org, ://.example.com:
协议名https:, data:
关键字 'self''self'
关键字 'none''none'

多个值也可以并列,用空格分隔

Content-Security-Policy: script-src 'self' https://apis.google.com

script-src 的特殊值

指令描述
'unsafe-inline'允许执行页面内嵌的 <script> 标签和事件监听函数
'unsafe-eval'允许将字符串当作代码执行,比如使用 evalsetTimeoutsetIntervalFunction 等函数。
nonce每次 HTTP 回应给出一个授权 token ,页面内嵌脚本必须有这个 token ,才会执行
hash列出允许执行的脚本代码的 Hash 值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。

注意事项

  • script-srcobject-src 是必设的,除非设置了 default-src

    因为攻击者只要能注入脚本,其他限制都可以规避。而 object-src 必设是因为 Flash 里面可以执行外部脚本

  • script-src 不能使用 unsafe-inline 关键字(除非伴随一个 nonce 值),也不能允许设置 data:URL

    <!-- 两个恶意攻击的例子 -->
    <img src="x" onerror="evil()" />
    <script src="data:text/javascript,evil()"></script>
  • 必须特别注意 JSONP 的回调函数

    <!-- 加载的脚本来自当前域名,但是通过改写回调函数,攻击者依然可以执行恶意代码 -->
    <script src="/path/jsonp?callback=alert(document.domain)//"></script>

拓展: Content-Security-Policy-Report-Only

除了 Content-Security-Policy ,还有一个 Content-Security-Policy-Report-Only 字段,表示不执行限制选项,只是记录违反限制的行为。它必须与 report-uri 选项配合使用

Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;

浏览器指纹 ( Browser Fingerprinting )

是什么

一种在线追踪技术,通过收集和分析用户浏览器的特定配置信息来唯一标识或区分不同的网络客户端,使用的信息包括但不限于以下内容:

  • 浏览器型号以及版本
  • 操作系统以及版本
  • 屏幕分辨率
  • 字体列表: 使用 CSS3 的@font-face 规则可以检测
  • 浏览器插件、扩展列表
  • 时区设置
  • 系统语言
  • WebGL 图形渲染信息
  • HTTP 请求头部分字段: 如 UA
  • HTML5 暴露的信息: 如 canvas 指纹、 audio 指纹等

通过这些信息,即使用户清除 cookie 或者使用无痕模式,也难以完全避免被识别、跟踪。

价值

理论上,未登录的状态下,并且浏览器中无法读取或存储持久 cookie ,无法读取客户端的 IP ,或同一个设备上切换不同的浏览器的情况下,我们仍然可以通过浏览器指纹完全或者部分识别单个设备,主要的应用场景如下:

  • 用户跟踪与识别: 例如在一个内容分发网站上,用户 A 喜欢浏览二次元的内容,通过浏览器指纹记录这个兴趣,那么下次用户不需要登录即可向 A 用户推送二次元的信息
  • 安全性与防欺诈: 在需要高安全性的领域中,浏览器指纹技术可以帮助企业 识别潜在的恶意用户或机器人 ,通过对访问请求进行指纹识别来提高系统的安全性和防止欺诈交易。
  • 合规与监管: 在某些法律要求严格的地区,通过浏览器指纹可以辅助验证用户身份,确保服务仅向符合年龄、地域等条件的用户提供,从而达到合规目的。
  • 数据分析: 企业和研究机构可以通过收集并分析浏览器指纹数据,了解用户群体的行为模式、设备分布以及市场趋势等信息。

发展进程

状态化追踪

主要依赖于用户登录时产生的 Cookie 和 evercookie 等技术进行追踪,这些信息需要用户的交互行为(如登录)才能获得。

静态浏览器指纹

通过收集浏览器的基本属性,如 User-Agent 字符串、插件信息、系统字体列表等 具有区分度的特征值来构建指纹 。这些特征相对稳定,不随用户会话或浏览历史而改变。

动态浏览器指纹

利用 Canvas 绘图、 WebGL 渲染、音频上下文等多种 HTML5 API 暴露出来的细微差异性数据生成指纹。这些动态特征能 捕捉到浏览器更深层次的信息,包括硬件相关的特性 ,从而极大提高了用户识别的准确度。

行为指纹

引入了用户行为模式作为指纹的一部分,例如鼠标移动轨迹、页面滚动速度、键盘敲击间隔等非设备固有特征。行为指纹 不仅包含静态和动态特征,还结合了时间序列的行为数据 ,使得追踪更为精准且难以规避。

采集基本指纹

浏览器基本指纹是指那些不依赖于用户交互或网站存储的本地数据(如 Cookie),仅仅通过 分析客户端浏览器的基本配置信息就可以收集到的 、用于区分不同浏览器环境的一组属性,常见信息以及采集方式见下表:

类型属性获取方式
浏览器特征User-Agentnavigator.userAgent
浏览器特征浏览器语言navigator.language
浏览器特征插件列表navigator.plugins
系统特征操作系统信息navigator.platform
时区特征本地时间与格林威治时间差(分钟)new Date().getTimezoneOffset()
时区特征完整时区信息需查询服务器获取
时区特征地理位置经纬度navigator.geolocation.getCurrentPosition
时区特征地理区域名称需查询服务器获取
时区特征IP 地址需通过服务器接口获取
硬件特征最大同时触摸点数navigator.maxTouchPoints
硬件特征可用逻辑处理器核心数navigator.hardwareConcurrency
屏幕特征屏幕宽高screen.widthscreen.height
屏幕特征屏幕分辨率与色彩深度组合screen.width、screen.height、screen.colorDepth
  1. 对于需要通过服务器获取的信息,客户端 JavaScript 通常无法直接获取详细地理区域、 IP 地址以及完整时区信息。
  2. 使用 navigator.geolocation.getCurrentPosition 获取经纬度时,需要用户的明确授权,并且在支持该 API 且运行在安全环境下的浏览器中调用。

采集高级指纹

Canvas 指纹

通过调用 Canvas API,绘制图形并获取渲染后的像素数据,不同浏览器和设备组合会导致细微差异。

const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.textBaseline = 'top'
ctx.font = '14px Arial'
ctx.fillText('Hello, world', 0, 0)

// imageData 为一维数组,包含了Canvas渲染后的像素信息
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data

WebGL 指纹

利用 WebGL API 获取图形处理器相关信息,不同的 GPU 会产生独特的着色器编译结果。

const gl = document.createElement('canvas').getContext('webgl')
const fingerprint = {}
fingerprint.vendor = gl.getParameter(gl.VENDOR)
fingerprint.renderer = gl.getParameter(gl.RENDERER)
// 可以进一步提取更多与硬件相关的参数

参考: fingerprint.js 源码

音频指纹

通过 AudioContext 分析音频处理能力,不同设备的音频 API 可能存在差异性。

const audioContext = new (window.AudioContext || window.webkitAudioContext)()
const fingerprint = audioContext.destination.channelCount

参考: fingerprint.js 源码

WebRTC

WebRTC ( Web Real-Time Communication ) 是一种允许网页浏览器进行实时通信的技术,无需安装插件或软件。

它使得用户能够在浏览器之间直接交换音频、视频和数据流,支持点对点的音视频通话、屏幕共享等功能。

通过 navigator.mediaDevices.getUserMedia() API 或者相关的 WebRTC 三个主要的 APIMediaStreamRTCPeerConnectionRTCDataChannel 接口,可以访问到一些设备特定的信息,例如:

  • 本地 IP 地址: WebRTC 实现中可能暴露出本地网络配置的一部分,如局域网 IPv4 或 IPv6 地址。
  • 硬件编解码器信息: 不同的设备可能支持不同的音频和视频编解码器,这些信息可以通过 WebRTC 获取。

现代浏览器已经采取措施限制 WebRTC 泄漏过多的本地设备信息,比如不再暴露本地 IP 地址

if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
  navigator.mediaDevices.enumerateDevices().then((devices) => {
    devices.forEach((device) => {
      console.log(
        `${device.kind}: ${device.label} id = ${device.deviceId}`
      )

      // 对于音频输入设备,还可以查询其具体属性
      if (device.kind === 'audioinput') {
        // 不建议在实际应用中尝试获取IP等敏感信息
        // 这里仅作为展示设备信息的例子
        // WebRTC IP泄露问题已经得到现代浏览器的关注并被逐步修复
      }
    })
  })
}

参考文献