nonce值必须由服务端每次请求动态生成,使用密码学安全随机函数生成并绑定到当前响应上下文,响应头与script标签中的nonce值需逐字节一致,否则脚本将静默失败。

nonce值必须由服务端每次请求动态生成
硬编码、前端生成、复用 nonce 等同于没开 CSP。浏览器校验的是“一次性高熵令牌”,不是字符串本身。一旦攻击者拿到一次有效的 nonce,就能批量注入脚本。
正确做法是:在请求生命周期内调用密码学安全随机函数生成,例如:
- Node.js:
crypto.randomBytes(32).toString('hex')(避免 Base64 中的+、/、=) - Python:
secrets.token_urlsafe(32) - PHP:
bin2hex(random_bytes(32))
生成后必须立刻绑定到本次响应上下文(如 Express 的 res.locals.nonce、FastAPI 的 request.state.nonce),不能缓存、不能跨请求复用。
响应头与 script 标签的 nonce 值必须逐字节一致
浏览器验证是原子级比对,任何微小差异都会导致脚本静默失败——不报错、不警告,只拒绝执行。
立即学习“前端免费学习笔记(深入)”;
常见不一致点:
- 响应头写成
Content-Security-Policy: script-src 'nonce-abc123 '(末尾空格) vs<script nonce="abc123"></script> - 大小写混用:
'nonce-Abc123'vsnonce="abc123" - 模板插值时未转义,比如 Jinja2 里
{{ nonce }}含引号或换行,导致 HTML 标签提前闭合 - 误用
<meta http-equiv="Content-Security-Policy">—— 该方式完全不支持 nonce
验证方法:用浏览器 DevTools 的 Network → Headers 查响应头;再右键“查看网页源代码”搜索 <script nonce="</code">,对比两处值是否完全相同(包括大小写、无空格、无换行)。</script>
Webpack/Vite 的 runtime 脚本必须显式携带 nonce
你手动写的 <script nonce="xxx"></script> 加了属性,但 Webpack 打包生成的 __webpack_require__、Vite 的 HMR 初始化逻辑、Vue/React 的 hydration 脚本,默认都是内联形式,且不带 nonce。它们会被 CSP 拦截,造成白屏或功能异常。
解决方案分两步:
- 告诉打包工具当前 nonce 值:
Webpack 设置全局变量__webpack_nonce__ = 'xxx'(需在入口 JS 执行前注入);
Vite 需配合插件(如vite-plugin-csp)或手动在 HTML 模板中注入 - 确保最终输出的
<script></script>标签也渲染出nonce="xxx"属性——仅 JS 变量不够,HTML 标签属性必须存在 - 响应头中的
script-src必须包含'nonce-xxx',否则__webpack_nonce__无效
strict-dynamic 是放行动态创建脚本的必要条件
nonce 只能放行初始 HTML 中已存在的 <script nonce="xxx"></script>。后续用 document.createElement('script') 或框架动态插入的脚本,仍会被拦截。
要让它们合法执行,必须启用 strict-dynamic,且写法有强约束:
- 错误写法:
script-src 'self' 'strict-dynamic'——'self'会让浏览器忽略'strict-dynamic' - 正确写法:
script-src 'nonce-xxx' 'strict-dynamic' - 首个执行的脚本(通常是首屏初始化脚本)必须带该 nonce,否则后续所有动态脚本都会被拒
注意:strict-dynamic 对 onclick、onload 等内联事件处理器无效,这类仍需重构为事件监听器。
真正难的不是加 nonce,而是确保服务端生成、响应头注入、HTML 渲染、构建工具适配、框架 hydration 这五环全部咬合——漏一环,CSP 就形同虚设。



















