XSS 攻击与 CSP 防御

面试先答

XSS(跨站脚本攻击)的本质是:不可信数据进入了 HTML、属性、URL 或 JavaScript 等可执行上下文,被浏览器当成代码解析或执行

防御时要先阻断漏洞产生,再限制漏洞被利用后的影响:

  1. 根据输出上下文进行编码,避免数据变成代码。
  2. 优先使用 textContent 等安全 API;确实要展示富文本时,使用成熟的 HTML 净化方案。
  3. 谨慎使用 innerHTMLv-htmldangerouslySetInnerHTML 等危险入口。
  4. 为 Cookie 设置 HttpOnlySecureSameSite,降低凭证泄露和跨站利用风险。
  5. 部署严格的 CSP,作为 XSS 的纵深防御,限制页面可以执行的脚本及加载的资源。

CSP 能显著降低 XSS 的可利用性,但不能代替编码、净化和安全 API。

XSS 成立的条件

一次典型的 XSS 通常同时满足两个条件:

  • 攻击者可以控制一段输入,例如 URL 参数、评论、昵称或 location.hash
  • 应用把这段输入送入了危险的输出位置,并让浏览器按代码而不是纯文本处理。

例如,下面的代码把 URL 参数直接交给了 HTML 解析器:

const params = new URLSearchParams(location.search)
const name = params.get('name') ?? ''

document.querySelector('#greeting').innerHTML = `你好,${name}`

攻击者只要构造包含 HTML 的 name 参数,就可能注入标签或事件处理器。这里真正的问题不是“读取了 URL”,而是把不可信字符串传给了 innerHTML

如果需求只是显示文本,应改用:

const params = new URLSearchParams(location.search)
const name = params.get('name') ?? ''

document.querySelector('#greeting').textContent = `你好,${name}`

三种常见类型

类型恶意数据来自哪里是否经过服务端典型场景
反射型 XSS当前请求中的 URL、表单等输入通常会,服务端将输入反射到响应搜索结果页、错误提示页
存储型 XSS已写入数据库、评论或用户资料的数据通常会,读取后输出给访问者评论区、富文本内容、后台系统
DOM 型 XSSURL、消息事件、存储等前端数据源不一定,漏洞可完全发生在浏览器location.hash 被写入 innerHTML

反射型 XSS

攻击载荷来自当前请求,并被页面响应立即返回。它通常需要攻击者诱导用户访问特制链接。

例如,服务端把 keyword 参数未经编码地拼进搜索结果页面。浏览器收到响应后,会把其中的攻击字符串当成 HTML 解析。

存储型 XSS

攻击载荷先被保存,之后每次展示数据时触发。攻击者只需投毒一次,就可能影响所有访问相关页面的用户,因此通常危害更大。

保存原始用户输入本身不一定是漏洞;危险往往发生在输出阶段。同一份数据进入 HTML 文本、HTML 属性、URL 或 JavaScript 字符串时,需要采用对应上下文的安全处理方式。

DOM 型 XSS

DOM 型 XSS 的漏洞位于前端数据流中:不可信数据从 source 流入危险 sink。

常见 source 包括:

  • location.searchlocation.hashdocument.referrer
  • postMessage 的消息内容
  • localStoragesessionStorage
  • 接口返回或页面中可被攻击者控制的数据

常见危险 sink 包括:

  • innerHTMLouterHTMLinsertAdjacentHTML()document.write()
  • eval()new Function()、字符串形式的 setTimeout()
  • 不可信的 javascript: URL、内联事件处理器

反射型、存储型描述的是载荷的传播路径,DOM 型强调客户端的执行位置,所以它们并非严格互斥。例如,存储在服务端的数据也可能最终通过前端 innerHTML 触发 DOM 型 XSS。

XSS 能造成什么影响

XSS 脚本以当前页面的源和用户身份运行,常见后果包括:

  • 读取页面中的敏感信息,以及未设置 HttpOnly 的 Cookie。
  • 以当前用户身份发起请求、修改资料或执行敏感操作。
  • 篡改页面、伪造登录框,窃取用户输入。
  • 读取前端存储中的令牌,并向外部地址传输数据。
  • 利用同源权限访问该站点中本来受同源策略保护的接口数据。

HttpOnly 可以阻止 JavaScript 直接读取 Cookie,但无法阻止 XSS 脚本携带 Cookie 发起同源请求。因此它只能降低部分后果,不能修复 XSS。

第一层:从代码上消除 XSS

1. 按输出上下文编码

不要把“过滤掉 <script> 标签”当成通用方案。XSS 还可能出现在属性、URL、事件处理器等位置,简单黑名单很容易被绕过。

应根据数据最终进入的上下文处理:

  • HTML 文本节点:进行 HTML 实体编码,或直接使用 textContent
  • HTML 属性:使用 DOM 属性 API,并限制允许的属性和值。
  • URL:校验协议和目标范围,拒绝 javascript: 等危险协议。
  • JavaScript 上下文:避免拼接不可信数据,改为通过数据接口传递。

2. 优先使用安全 DOM API

只展示文本时,使用 textContentcreateTextNode();创建结构时,使用 createElement() 和明确的属性赋值。

const item = document.createElement('li')
item.textContent = userComment
document.querySelector('#comments').append(item)

如果业务必须展示用户提交的富文本,应使用成熟、持续维护的 HTML 净化库,并通过允许列表保留必要标签和属性。不要自己用正则表达式实现 HTML 净化器。

3. 注意框架的逃逸出口

React、Vue 等框架默认会转义普通模板插值,但以下能力会主动绕过保护:

  • React 的 dangerouslySetInnerHTML
  • Vue 的 v-html
  • 直接操作原生 DOM 的 innerHTML

使用这些能力时,输入必须是可信内容或已经过可靠净化的 HTML。

4. 降低凭证和敏感操作风险

  • 登录 Cookie 使用 HttpOnlySecure,并根据业务设置合适的 SameSite
  • 敏感操作增加二次确认、短期凭证或重新认证。
  • 不要在前端存储中长期保存高价值令牌。

这些措施用于降低攻击成功后的损失,不能替代 XSS 修复。

第二层:用 CSP 限制脚本执行

CSP(Content Security Policy,内容安全策略)由服务端通过响应头声明,浏览器据此限制脚本、样式、图片、连接和页面嵌套等能力。它的核心不是“发现恶意代码”,而是建立页面可以加载和执行什么的允许范围。

下面是一份用于说明思路的策略;真实响应头通常写在一行,并应按业务需要继续收紧:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{RANDOM}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; connect-src 'self' https://api.example.com; report-to csp-endpoint
指令作用
default-src其他资源类型未单独配置时的默认来源
script-src可以加载和执行脚本的来源,以及 nonce/hash 等授权方式
connect-srcfetch、XHR、WebSocket 等可连接的目标
object-src 'none'禁止插件类嵌入内容,减少不必要的执行入口
base-uri 'self'限制 <base>,避免相对 URL 的基准地址被注入篡改
frame-ancestors限制哪些页面可以通过 iframe 嵌入当前页面,防范点击劫持

nonce 与 hash

严格 CSP 不应为了兼容旧代码直接加入 'unsafe-inline''unsafe-eval',因为它们会重新放开常见的 XSS 执行路径。

对确实需要的内联脚本,可以使用 nonce:服务端为每次响应生成不可预测的随机值,同时写入 CSP 和被授权的 <script>

Content-Security-Policy: script-src 'self' 'nonce-r4nd0mValue'
<script nonce="r4nd0mValue">
  window.appConfig = { locale: 'zh-CN' }
</script>

nonce 不能写死或长期复用。内容固定的内联脚本也可以在 script-src 中声明内容 hash;脚本一旦改变,就需要同步更新 hash。

CSP 能防什么、不能防什么

CSP 可以阻止未授权来源的脚本、未授权的内联脚本以及未允许的字符串代码执行,从而让许多注入载荷无法运行。但它不是 XSS 净化器:

  • CSP 不会修复 innerHTML 等危险数据流。
  • 如果被允许的脚本源遭到入侵,宽松的来源规则仍可能被利用。
  • 过宽的通配符、'unsafe-inline''unsafe-eval' 会显著削弱策略。
  • 即使 Cookie 设置了 HttpOnly,已执行的脚本仍可能以用户身份操作页面和接口。

因此正确关系是:编码、净化和安全 API 用来消除漏洞,CSP 用来限制剩余漏洞的利用范围。

CSP 常见关联考点

第三方脚本与 SRI

CSP 回答“允许从哪里加载脚本”,SRI(Subresource Integrity)回答“加载到的文件是否仍是预期内容”。对于版本固定的 CDN 脚本,可以同时限制来源并使用 integrity 校验内容 hash。

<script
  src="https://cdn.example.com/library.min.js"
  integrity="sha384-{HASH}"
  crossorigin="anonymous"
></script>

两者互补:来源可信不代表该来源永远不会被篡改,而 SRI 也不能替代整页的资源加载策略。

iframe 与点击劫持

frame-ancestors 控制“谁可以嵌入当前页面”,常用于防范点击劫持;frame-src 控制“当前页面可以嵌入谁”;iframe 的 sandbox 则限制被嵌入页面获得的能力。三者方向不同,不能混为一谈。更多内容见网页沙箱与 iframe 安全

同源策略、CORS 与 CSRF

  • 同源策略限制跨源页面读取数据,CORS 是服务端授权部分跨源读取的机制。
  • CSP 主要限制当前页面能加载、执行或连接哪些资源。
  • CSRF 利用浏览器自动携带身份凭证诱导用户发起请求,防御重点是 CSRF Token、SameSite 和来源校验。

XSS 往往可以在站点自身上下文中读取 Token 或直接调用接口,因此出现 XSS 时,许多 CSRF 防线也可能被绕过。

CSP 如何平滑上线

直接启用严格策略可能误伤现有脚本和第三方资源。通常先部署 Content-Security-Policy-Report-Only:浏览器只上报违规,不执行拦截。

推荐流程:

  1. 先收集违反策略的报告,区分正常业务资源、浏览器扩展噪声与真实异常。
  2. 清理不必要的内联脚本、动态代码执行和过宽来源。
  3. 补齐 nonce、hash 及必要的资源允许范围。
  4. 切换为正式的 Content-Security-Policy,并继续监控报告。

违规报告只是排查信号,不等于已经发生攻击。

常见追问

CSP 能彻底解决 XSS 吗?

不能。严格 CSP 能显著降低 XSS 的可利用性,但错误配置、可信脚本源被攻破或已有业务脚本中的危险能力都可能削弱它。根本防线仍是上下文编码、可靠净化和安全 DOM API。

输入过滤和输出编码哪个更重要?

输入校验适合保证业务数据格式,输出编码负责保证数据在特定上下文中不会变成代码。仅依赖输入过滤无法覆盖数据被复用到不同输出上下文的情况。

HttpOnly 能防 XSS 吗?

不能。它只能阻止脚本直接读取 Cookie;XSS 仍能操作 DOM、读取其他敏感数据,并携带 Cookie 发起同源请求。

为什么不推荐'unsafe-inline'

它允许大量内联脚本和事件处理器执行,使攻击者注入的代码更容易运行。应逐步把脚本外置,或使用每个响应唯一的 nonce、内容 hash 来精确授权。

总结

回答 XSS 时可以沿着这条主线展开:

XSS 是不可信数据被浏览器当成代码执行。反射型、存储型描述载荷来源和传播路径,DOM 型强调客户端 source 到危险 sink 的数据流。根本防御是按上下文编码、使用安全 DOM API,并对富文本可靠净化;Cookie 属性用于降低损失;严格 CSP 再通过来源限制和 nonce/hash 阻断未授权脚本,作为纵深防御。