XSS 攻击与 CSP 防御
面试先答
XSS(跨站脚本攻击)的本质是:不可信数据进入了 HTML、属性、URL 或 JavaScript 等可执行上下文,被浏览器当成代码解析或执行。
防御时要先阻断漏洞产生,再限制漏洞被利用后的影响:
- 根据输出上下文进行编码,避免数据变成代码。
- 优先使用
textContent等安全 API;确实要展示富文本时,使用成熟的 HTML 净化方案。 - 谨慎使用
innerHTML、v-html、dangerouslySetInnerHTML等危险入口。 - 为 Cookie 设置
HttpOnly、Secure、SameSite,降低凭证泄露和跨站利用风险。 - 部署严格的 CSP,作为 XSS 的纵深防御,限制页面可以执行的脚本及加载的资源。
CSP 能显著降低 XSS 的可利用性,但不能代替编码、净化和安全 API。
XSS 成立的条件
一次典型的 XSS 通常同时满足两个条件:
- 攻击者可以控制一段输入,例如 URL 参数、评论、昵称或
location.hash。 - 应用把这段输入送入了危险的输出位置,并让浏览器按代码而不是纯文本处理。
例如,下面的代码把 URL 参数直接交给了 HTML 解析器:
攻击者只要构造包含 HTML 的 name 参数,就可能注入标签或事件处理器。这里真正的问题不是“读取了 URL”,而是把不可信字符串传给了 innerHTML。
如果需求只是显示文本,应改用:
三种常见类型
反射型 XSS
攻击载荷来自当前请求,并被页面响应立即返回。它通常需要攻击者诱导用户访问特制链接。
例如,服务端把 keyword 参数未经编码地拼进搜索结果页面。浏览器收到响应后,会把其中的攻击字符串当成 HTML 解析。
存储型 XSS
攻击载荷先被保存,之后每次展示数据时触发。攻击者只需投毒一次,就可能影响所有访问相关页面的用户,因此通常危害更大。
保存原始用户输入本身不一定是漏洞;危险往往发生在输出阶段。同一份数据进入 HTML 文本、HTML 属性、URL 或 JavaScript 字符串时,需要采用对应上下文的安全处理方式。
DOM 型 XSS
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 能造成什么影响
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
只展示文本时,使用 textContent、createTextNode();创建结构时,使用 createElement() 和明确的属性赋值。
如果业务必须展示用户提交的富文本,应使用成熟、持续维护的 HTML 净化库,并通过允许列表保留必要标签和属性。不要自己用正则表达式实现 HTML 净化器。
3. 注意框架的逃逸出口
React、Vue 等框架默认会转义普通模板插值,但以下能力会主动绕过保护:
- React 的
dangerouslySetInnerHTML - Vue 的
v-html - 直接操作原生 DOM 的
innerHTML
使用这些能力时,输入必须是可信内容或已经过可靠净化的 HTML。
4. 降低凭证和敏感操作风险
- 登录 Cookie 使用
HttpOnly和Secure,并根据业务设置合适的SameSite。 - 敏感操作增加二次确认、短期凭证或重新认证。
- 不要在前端存储中长期保存高价值令牌。
这些措施用于降低攻击成功后的损失,不能替代 XSS 修复。
第二层:用 CSP 限制脚本执行
CSP(Content Security Policy,内容安全策略)由服务端通过响应头声明,浏览器据此限制脚本、样式、图片、连接和页面嵌套等能力。它的核心不是“发现恶意代码”,而是建立页面可以加载和执行什么的允许范围。
下面是一份用于说明思路的策略;真实响应头通常写在一行,并应按业务需要继续收紧:
nonce 与 hash
严格 CSP 不应为了兼容旧代码直接加入 'unsafe-inline' 或 'unsafe-eval',因为它们会重新放开常见的 XSS 执行路径。
对确实需要的内联脚本,可以使用 nonce:服务端为每次响应生成不可预测的随机值,同时写入 CSP 和被授权的 <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。
两者互补:来源可信不代表该来源永远不会被篡改,而 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:浏览器只上报违规,不执行拦截。
推荐流程:
- 先收集违反策略的报告,区分正常业务资源、浏览器扩展噪声与真实异常。
- 清理不必要的内联脚本、动态代码执行和过宽来源。
- 补齐 nonce、hash 及必要的资源允许范围。
- 切换为正式的
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 阻断未授权脚本,作为纵深防御。

