XSS,也就是跨站脚本攻击,本质上是浏览器把不可信输入解释成了代码。

漏洞通常出现在“用户可以写,页面又会展示”的位置,例如搜索框、评论区、昵称、个人资料和消息提醒。只要输入进入 HTML、属性、URL 或 JavaScript 上下文,而输出处理不正确,就可能越过数据边界。

本文不提供可直接用于攻击的 XSS 载荷。安全验证应在授权环境中使用无害标记,重点证明执行条件,而不是窃取数据或影响真实用户。

一、XSS 成立需要什么条件

一个 XSS 问题通常同时满足以下条件:

  1. 不可信输入能够进入页面。
  2. 输入落在浏览器会解释的位置。
  3. 输出编码或过滤没有匹配当前上下文。

“用户可控”不只包括 URL 参数,还包括:

  • 表单字段。
  • Cookie 和请求头。
  • 数据库中的历史内容。
  • 文件上传后的文件名。
  • 第三方接口返回的数据。
  • WebSocket 消息。
  • postMessage 事件。
  • 前端路由和 URL 片段。

风险点不在于输入来自哪里,而在于它最终被放进了什么上下文。

二、常见类型

2.1 反射型 XSS

反射型 XSS 中,请求参数被服务端立即写回响应页面。恶意参数通常不在服务器长期保存,需要诱导用户访问包含该参数的链接。

典型链路:

请求参数
    -> 服务端模板
    -> 未正确编码的 HTML
    -> 浏览器执行

只在当前用户的浏览器中触发,并不意味着风险低。攻击者仍可能借此读取当前页面数据或诱导用户操作。

2.2 存储型 XSS

存储型 XSS 会把不可信内容保存到数据库、缓存或文件中,其他用户访问页面时再次输出。

典型场景:

  • 评论内容。
  • 用户昵称和签名。
  • 工单描述。
  • 文件名称。
  • 导入的数据。
  • 富文本编辑器。

它的影响范围可能更大,因为内容可能在多个页面和多个用户之间传播。

2.3 DOM 型 XSS

DOM 型 XSS 的主要处理发生在浏览器端。服务器返回的 HTML 可能完全正常,但前端脚本把不可信数据写入了危险位置。

常见数据源包括:

  • location.search
  • location.hash
  • document.referrer
  • window.name
  • postMessage 事件数据
  • 本地存储和 Session Storage

常见危险写入点包括:

  • innerHTML
  • outerHTML
  • document.write
  • 动态创建事件属性
  • 把字符串当作代码执行
  • 把外部内容插入 SVG 或 iframe

DOM 型 XSS 不能只靠服务端 HTML 编码解决。前端也必须根据最终插入位置进行安全处理。

三、输出上下文决定编码方式

“统一替换几个特殊字符”不是完整的 XSS 防御。不同上下文需要不同编码方式。

3.1 HTML 文本

用户内容进入普通文本节点时,应把 &、<、> 等字符转义,避免被解释成标签。

后端示例:

$safe = htmlspecialchars(
    $input,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

该函数适合普通 HTML 文本和大部分属性值,但不能直接解决 JavaScript、CSS 和 URL 中的所有问题。

3.2 HTML 属性

属性需要同时处理引号和上下文。更安全的做法是:

  • 尽量用模板引擎自动转义。
  • 属性值始终使用引号包围。
  • 不把用户输入拼接到事件属性。
  • 对 URL 属性执行协议白名单。

例如,用户提交的链接不应允许危险协议或外部脚本执行。

3.3 JavaScript 上下文

不要在脚本字符串中直接插入未经处理的用户输入。更好的方式是:

  1. 服务端返回结构化 JSON。
  2. 前端通过安全 API 读取。
  3. 使用 textContent 写入文本。
  4. 不把用户输入传给代码执行函数。

安全文本写入示例:

const titleElement = document.querySelector("#post-title");
titleElement.textContent = postTitle;

textContent 会按文本处理内容,不会把其中的标签解析为 HTML。

3.4 URL 上下文

URL 参数需要先编码,并验证协议。允许的协议应明确写在白名单中。

不要直接依赖客户端提供的跳转地址。开放重定向与 XSS 组合后可能扩大影响。

3.5 富文本

富文本编辑器通常必须允许一部分 HTML,因此不能简单依赖普通文本编码。应采用:

  • 结构化编辑器输出。
  • 服务端 HTML 清洗。
  • 标签和属性允许列表。
  • 禁止脚本、事件属性和危险 URL。
  • 定期测试清洗规则。

只用黑名单删除 <script> 远远不够。

四、过滤与黑名单为什么不可靠

黑名单通常假设攻击者只会使用几种固定写法,但浏览器和 HTML 解析器支持的语法更复杂。

常见问题包括:

  • 标签或属性的大小写变化。
  • 多重编码。
  • 被过滤后重新拼接出的危险结构。
  • 事件属性和资源地址。
  • 不同浏览器对异常 HTML 的容错。
  • 输入经过多个代理和组件重复解码。

正确方向不是在输入中寻找所有“坏词”,而是:

  1. 明确允许的数据格式。
  2. 在正确输出位置进行编码。
  3. 对富文本做结构化清洗。
  4. 用浏览器安全策略作为第二层防线。

五、前端安全 API

5.1 DOM 写入

优先使用安全文本接口:

element.textContent = value;
element.setAttribute("title", value);

只有在内容已经过可信清洗时,才考虑插入 HTML。不要直接使用:

element.innerHTML = untrustedValue;

5.2 事件处理

应通过 addEventListener 绑定函数:

button.addEventListener("click", handleClick);

不要把用户字符串当作事件代码,也不要根据不可信数据动态生成属性。

5.3 消息事件

使用 postMessage 时应验证:

  • 消息来源 origin。
  • event.source 是否符合预期。
  • 消息字段结构和类型。
  • 消息是否只包含当前页面需要的数据。

不要接收任意来源的消息后直接写入页面或执行操作。

六、浏览器侧纵深防御

会话 Cookie 应设置:

  • HttpOnly:降低脚本读取 Cookie 的风险。
  • Secure:只通过 HTTPS 发送。
  • SameSite:限制跨站请求携带范围。

这些属性不能修复 XSS,但可以降低会话数据泄漏和跨站请求风险。

6.2 内容安全策略

CSP 可以限制脚本来源和内联脚本执行。合理的策略通常包括:

  • 默认只允许明确列出的脚本源。
  • 避免不必要的 unsafe-inline 和 unsafe-eval。
  • 对第三方脚本单独评估。
  • 使用 nonce 或 hash 管理必要的内联脚本。
  • 先以报告模式观察,再逐步收紧。

CSP 是重要防线,但不能替代输出编码。如果应用允许来自同一来源的危险脚本,CSP 仍可能被绕过。

6.3 安全响应头

可结合以下响应头降低浏览器攻击面:

  • Content-Security-Policy
  • X-Content-Type-Options: nosniff
  • Referrer-Policy
  • 合理的 Permissions-Policy

是否使用 X-Frame-Options 或 CSP 的 frame-ancestors,取决于页面是否需要被嵌入。

6.4 敏感操作保护

对于修改密码、转账、删除数据等操作,应:

  • 要求有效会话。
  • 校验 CSRF 防护令牌。
  • 对高风险操作重新认证。
  • 记录审计日志。
  • 不把用户角色和资源归属交给前端决定。

七、工具在测试中的作用

BeEF 等浏览器安全测试工具可以观察 XSS 触发后的浏览器行为。这类工具只能在明确授权的测试环境中使用。

在合法测试中,应优先使用无害标记证明执行条件,例如写入一个独立测试页面可见的静态提示。不要读取真实 Cookie、监听键盘、截屏或诱导用户。

测试报告应说明:

  • 漏洞所在的输入和输出位置。
  • 触发时需要的前置条件。
  • 是否可持久化。
  • 影响范围。
  • 修复建议。
  • 修复后的复测结果。

八、检测与监控

建议记录:

  • 输入来源和输出页面。
  • 用户、会话和请求 ID。
  • 被清洗规则拦截的字段。
  • CSP 报告。
  • 异常跳转和动态脚本创建。
  • 大量相似请求或自动化探测。

WAF 日志可以提供线索,但不应作为唯一修复手段。XSS 的根因仍然在输出上下文和数据流。

九、防御清单

  • 所有输入都有明确的输出位置。
  • 编码方式与 HTML、属性、URL 或 JavaScript 上下文匹配。
  • 富文本使用结构化清洗和允许列表。
  • 前端优先使用 textContent 等安全写入方式。
  • 不执行来自 URL、存储或消息事件的任意字符串。
  • 会话 Cookie 启用 HttpOnly、Secure 和 SameSite。
  • 配置并持续维护 CSP。
  • 第三方脚本经过安全评估。
  • 敏感操作有 CSRF 防护和重新认证。
  • 前端过滤没有替代服务端输出编码。
  • 安全测试使用无害验证标记,不影响真实用户。

十、小结

XSS 的防御重点不是删除某个标签,而是理解数据从输入到浏览器执行的完整路径。

同一条用户数据进入 HTML 文本、属性、URL 和 JavaScript 时,需要不同的处理方式。只有在正确上下文完成编码,再叠加 CSP、Cookie 属性和安全的前端 API,才能形成稳定的纵深防御。