跳到内容

第 8 卷 / 第 4 章 / 第 1 课

4.1 浏览器安全边界:同源策略、XSS 与 CSP ​

浏览器同时打开银行、邮箱和陌生网页,却要阻止陌生网页读取其他站点的数据。Web 安全的许多机制都建立在一个核心边界上:origin。

Origin 与 Site 不是同一个概念 ​

对普通 HTTP(S) 页面,origin 由 scheme、host、port 组成;未显式写端口时使用协议默认端口。data: 等 URL 可能具有不透明 origin,不能照搬这个三元组:

text
https://app.example.com:443

任一部分不同就是不同 origin。同源策略限制脚本读取跨源响应和 DOM,但允许某些跨源请求被发送,也允许页面加载图片、样式和脚本等资源。

Cookie 的 SameSite 使用的是 site 概念,通常围绕可注册域及 scheme 判断。a.example.com 与 b.example.com 可以跨 origin,却仍可能 same-site。因此“同源策略会阻止 CSRF”和“SameSite 能隔离不可信子域”都不成立。

XSS 是攻击者代码进入受信 origin ​

一旦攻击者 JavaScript 在应用 origin 中执行,它可以读取 DOM、Web Storage 和非 HttpOnly Cookie,调用同源 API、修改界面并代表用户操作。HttpOnly 只阻止脚本直接读取该 Cookie,不会给页面数据加保护,也不阻止浏览器带 Cookie 发请求。常见入口:

  • Stored XSS:恶意内容被持久化,之后展示给其他用户;
  • Reflected XSS:请求参数未经安全处理进入响应;
  • DOM-based XSS:客户端把不可信数据送入危险 DOM sink。

根因不是“没有过滤 <script>”,而是把不可信数据当成某种可执行语法的一部分。

输出编码必须匹配上下文 ​

同一个字符串进入不同位置,需要不同处理:

html
<div>HTML text context</div>
<a href="URL attribute context">link</a>
<script>JavaScript context</script>
<style>CSS context</style>

HTML text encoding 不能安全地替代 JavaScript string、URL 或 CSS 编码;对 URL 还要验证协议与目的地,单做百分号编码不会让 javascript: 变安全。优先让模板引擎按上下文自动转义,并避免把用户数据直接放入 inline script/style。

在浏览器端优先使用不会解析 HTML 的 sink:

js
element.textContent = untrustedText

而不是:

js
element.innerHTML = untrustedText

若产品必须接受富文本,应使用受维护、按明确 allowlist 配置的 HTML sanitizer,并对 sanitizer 和浏览器解析差异持续测试。正则表达式无法可靠解析 HTML。

URL 同样可能成为执行入口 ​

设置 href、src 或跳转目标前,要解析 URL 并限制 scheme、host 与用途。javascript:、data:、协议相对 URL、用户信息段和混淆编码都可能绕过简单前缀判断。

外链新窗口显式使用 rel="noopener",防止新页面控制 opener;现代浏览器的 target="_blank" 通常已隐式提供这一行为。重定向目标使用服务端映射或严格 allowlist,不能把任意 next 参数原样写进 Location。

CSP 是纵深防御,不是编码替代品 ​

可从 report-only 开始收集违反情况,再逐步收紧:

http
Content-Security-Policy:
  default-src 'none';
  script-src 'nonce-<per-response-random-value>' 'strict-dynamic';
  style-src 'self';
  img-src 'self' https: data:;
  connect-src 'self' https://api.example.com;
  base-uri 'none';
  object-src 'none';
  frame-ancestors 'none'

上面为阅读而折行,实际发送时应是一条完整 header 值;观察模式使用 Content-Security-Policy-Report-Only,不会阻止违规执行。Nonce 用 CSPRNG 每响应生成至少 128 bit 随机值,经 Base64 编码后同时写入策略和受信 script 的 nonce 属性。占位符不能原样上线,也不能把用户输入插进带 nonce 的脚本。

支持 CSP3 的浏览器会让 strict-dynamic 把信任传给这些脚本动态创建的脚本,并忽略同一 script-src 中的 host/scheme allowlist、self 和 unsafe-inline。因此受信加载器也不能接受任意用户 URL。未使用这套信任机制的策略若依赖宽泛来源或 unsafe-inline,仍容易被 JSONP、上传目录或被接管子域绕过。

Trusted Types 可通过 require-trusted-types-for 'script' 在支持的浏览器中限制指定 DOM XSS sink 只接受受控 policy 产生的值。它不会自动清理 HTML:policy 若把原字符串直接放行,危险内容仍会进入 sink。

第三方脚本拥有页面权限 ​

Analytics、客服组件和 tag manager 在页面 origin 中运行时,通常能访问与一方脚本相同的数据。Subresource Integrity 可以校验固定跨源资源内容:

html
<script
  src="https://cdn.example/widget.js"
  integrity="sha384-..."
  crossorigin="anonymous"></script>

这里的摘要是占位符,要用实际资源字节生成;跨源 SRI 还要求资源服务器通过 CORS 允许该读取。若与前面的 CSP 一起使用,此 script 还需要合适的 nonce 等 CSP 授权,SRI 不会替它放行。频繁动态更新的脚本难以 pin,SRI 也不限制脚本运行权限。第三方代码需要供应链评审、最小数据暴露和隔离策略;高风险组件可考虑 sandboxed iframe 与窄消息协议。

验证清单 ​

  • 每个不可信值最终进入哪个解析上下文?
  • 模板是否默认转义,哪里显式关闭了转义?
  • 客户端是否存在 innerHTML、document.write、字符串 eval 等 sink?
  • 富文本 sanitizer 是否有绕过回归测试?
  • CSP 是否基于 nonce/hash,而不是大范围 allowlist?
  • 第三方脚本是否真的需要在主 origin 运行?
  • XSS 成功后,HttpOnly、重新认证和资源级授权能否继续缩小影响?

下一课进入服务端输入边界,追踪 SQL、命令、模板与文件路径中的注入。

参考资料 ​

Built with VitePress | Software Systems Atlas