XSS(跨站脚本攻击)的本质是:不可信数据进入了 HTML、属性、URL 或 JavaScript 等可执行上下文,被浏览器当成代码解析或执行。
防御时要先阻断漏洞产生,再限制漏洞被利用后的影响:
textContent 等安全 API;确实要展示富文本时,使用成熟的 HTML 净化方案。innerHTML、v-html、dangerouslySetInnerHTML 等危险入口。HttpOnly、Secure、SameSite,降低凭证泄露和跨站利用风险。CSP 能显著降低 XSS 的可利用性,但不能代替编码、净化和安全 API。
一次典型的 XSS 通常同时满足两个条件:
location.hash。例如,下面的代码把 URL 参数直接交给了 HTML 解析器:
攻击者只要构造包含 HTML 的 name 参数,就可能注入标签或事件处理器。这里真正的问题不是“读取了 URL”,而是把不可信字符串传给了 innerHTML。
如果需求只是显示文本,应改用:
| 类型 | 恶意数据来自哪里 | 是否经过服务端 | 典型场景 |
|---|---|---|---|
| 反射型 XSS | 当前请求中的 URL、表单等输入 | 通常会,服务端将输入反射到响应 | 搜索结果页、错误提示页 |
| 存储型 XSS | 已写入数据库、评论或用户资料的数据 | 通常会,读取后输出给访问者 | 评论区、富文本内容、后台系统 |
| DOM 型 XSS | URL、消息事件、存储等前端数据源 | 不一定,漏洞可完全发生在浏览器 | location.hash 被写入 innerHTML |
攻击载荷来自当前请求,并被页面响应立即返回。它通常需要攻击者诱导用户访问特制链接。
例如,服务端把 keyword 参数未经编码地拼进搜索结果页面。浏览器收到响应后,会把其中的攻击字符串当成 HTML 解析。
攻击载荷先被保存,之后每次展示数据时触发。攻击者只需投毒一次,就可能影响所有访问相关页面的用户,因此通常危害更大。
保存原始用户输入本身不一定是漏洞;危险往往发生在输出阶段。同一份数据进入 HTML 文本、HTML 属性、URL 或 JavaScript 字符串时,需要采用对应上下文的安全处理方式。
DOM 型 XSS 的漏洞位于前端数据流中:不可信数据从 source 流入危险 sink。
常见 source 包括:
location.search、location.hash、document.referrerpostMessage 的消息内容localStorage、sessionStorage常见危险 sink 包括:
innerHTML、outerHTML、insertAdjacentHTML()、document.write()eval()、new Function()、字符串形式的 setTimeout()javascript: URL、内联事件处理器反射型、存储型描述的是载荷的传播路径,DOM 型强调客户端的执行位置,所以它们并非严格互斥。例如,存储在服务端的数据也可能最终通过前端 innerHTML 触发 DOM 型 XSS。
XSS 脚本以当前页面的源和用户身份运行,常见后果包括:
HttpOnly 的 Cookie。HttpOnly 可以阻止 JavaScript 直接读取 Cookie,但无法阻止 XSS 脚本携带 Cookie 发起同源请求。因此它只能降低部分后果,不能修复 XSS。
不要把“过滤掉 <script> 标签”当成通用方案。XSS 还可能出现在属性、URL、事件处理器等位置,简单黑名单很容易被绕过。
应根据数据最终进入的上下文处理:
textContent。javascript: 等危险协议。只展示文本时,使用 textContent、createTextNode();创建结构时,使用 createElement() 和明确的属性赋值。
如果业务必须展示用户提交的富文本,应使用成熟、持续维护的 HTML 净化库,并通过允许列表保留必要标签和属性。不要自己用正则表达式实现 HTML 净化器。
React、Vue 等框架默认会转义普通模板插值,但以下能力会主动绕过保护:
dangerouslySetInnerHTMLv-htmlinnerHTML使用这些能力时,输入必须是可信内容或已经过可靠净化的 HTML。
HttpOnly 和 Secure,并根据业务设置合适的 SameSite。这些措施用于降低攻击成功后的损失,不能替代 XSS 修复。
CSP(Content Security Policy,内容安全策略)由服务端通过响应头声明,浏览器据此限制脚本、样式、图片、连接和页面嵌套等能力。它的核心不是“发现恶意代码”,而是建立页面可以加载和执行什么的允许范围。
下面是一份用于说明思路的策略;真实响应头通常写在一行,并应按业务需要继续收紧:
| 指令 | 作用 |
|---|---|
default-src | 其他资源类型未单独配置时的默认来源 |
script-src | 可以加载和执行脚本的来源,以及 nonce/hash 等授权方式 |
connect-src | fetch、XHR、WebSocket 等可连接的目标 |
object-src 'none' | 禁止插件类嵌入内容,减少不必要的执行入口 |
base-uri 'self' | 限制 <base>,避免相对 URL 的基准地址被注入篡改 |
frame-ancestors | 限制哪些页面可以通过 iframe 嵌入当前页面,防范点击劫持 |
严格 CSP 不应为了兼容旧代码直接加入 'unsafe-inline' 或 'unsafe-eval',因为它们会重新放开常见的 XSS 执行路径。
对确实需要的内联脚本,可以使用 nonce:服务端为每次响应生成不可预测的随机值,同时写入 CSP 和被授权的 <script>。
nonce 不能写死或长期复用。内容固定的内联脚本也可以在 script-src 中声明内容 hash;脚本一旦改变,就需要同步更新 hash。
CSP 可以阻止未授权来源的脚本、未授权的内联脚本以及未允许的字符串代码执行,从而让许多注入载荷无法运行。但它不是 XSS 净化器:
innerHTML 等危险数据流。'unsafe-inline'、'unsafe-eval' 会显著削弱策略。HttpOnly,已执行的脚本仍可能以用户身份操作页面和接口。因此正确关系是:编码、净化和安全 API 用来消除漏洞,CSP 用来限制剩余漏洞的利用范围。
CSP 回答“允许从哪里加载脚本”,SRI(Subresource Integrity)回答“加载到的文件是否仍是预期内容”。对于版本固定的 CDN 脚本,可以同时限制来源并使用 integrity 校验内容 hash。
两者互补:来源可信不代表该来源永远不会被篡改,而 SRI 也不能替代整页的资源加载策略。
frame-ancestors 控制“谁可以嵌入当前页面”,常用于防范点击劫持;frame-src 控制“当前页面可以嵌入谁”;iframe 的 sandbox 则限制被嵌入页面获得的能力。三者方向不同,不能混为一谈。更多内容见网页沙箱与 iframe 安全。
SameSite 和来源校验。XSS 往往可以在站点自身上下文中读取 Token 或直接调用接口,因此出现 XSS 时,许多 CSRF 防线也可能被绕过。
直接启用严格策略可能误伤现有脚本和第三方资源。通常先部署 Content-Security-Policy-Report-Only:浏览器只上报违规,不执行拦截。
推荐流程:
Content-Security-Policy,并继续监控报告。违规报告只是排查信号,不等于已经发生攻击。
不能。严格 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 阻断未授权脚本,作为纵深防御。