结论:动态脚本本身不“劫持”,真正被劫持的是执行上下文——关键在控制 script 标签的来源、执行时机和作用域;inline script 与 document.write 组合高危,因后者会阻塞解析并重写文档流,若内容源自用户输入,则等于主动交出执行权。

直接说结论:动态脚本本身不“劫持”,真正被劫持的是执行上下文——关键在控制 script 标签的来源、执行时机和作用域,而不是靠禁用或混淆。
为什么 inline script + document.write 是高危组合
很多审查流程一看到 document.write 就打红标,但真正危险的是它和未受控的内联脚本一起出现。浏览器解析到 document.write 时会阻塞并重写当前文档流,如果此时 script 内容来自用户输入或拼接字符串,就等于把执行权交给了不可信数据。
- 常见错误现象:
document.write('<script>' + userInput + '</script>')—— 这不是“插入脚本”,这是主动求劫持 - 使用场景:老式广告加载、第三方统计埋点、CMS 模板渲染
- 参数差异:传入纯字符串 vs 传入已校验的函数引用(如
document.write(scriptEl.outerHTML))风险天差地别 - 性能影响:
document.write在现代页面中会强制同步重排,且无法被 preload 或 prefetch 提前感知
审查 src 属性时必须验证协议与域名白名单
只检查 script 标签有没有 src 属性远远不够。攻击者常利用相对路径、协议相对 URL 或子域名跳转绕过简单过滤。
- 常见错误现象:
<script src="//cdn.example.com/bundle.js"></script>—— 缺少协议,可能被中间人替换为http://并注入 - 必须拒绝:
javascript:伪协议、data:URI、blob:引用、任何含%编码或双斜杠的非标准路径 - 白名单应精确到 host + port(如
cdn.jsdelivr.net:443),而非仅匹配域名后缀 - 对
import动态导入也适用同样规则:import('/js/feature.js')中的路径需走同套校验逻辑
CSP 的 script-src 和 unsafe-inline 不能共存
很多团队开了 CSP 却仍允许 'unsafe-inline',等于给所有内联脚本发了免检通行证。CSP 不是“开了就安全”,而是策略必须闭环。
立即学习“前端免费学习笔记(深入)”;
- 常见错误现象:响应头含
Content-Security-Policy: script-src 'self' 'unsafe-inline',同时页面里有onclick="doSubmit()" - 正确做法:要么彻底移除所有内联事件和
style属性中的 JS,要么用nonce或hash精确授权特定脚本块 - 注意兼容性:
nonce需服务端每次响应生成唯一值,且必须同步注入到 HTML 和 CSP 头中;hash对压缩后代码敏感,CI/CD 流程需确保哈希计算发生在最终打包之后 - 不要依赖
'strict-dynamic'单独兜底——它只对由可信脚本动态创建的子资源生效,对原始 HTML 中的script标签无效
动态创建 script 元素时必须校验 src 再 append
用 document.createElement('script') 加载远程脚本看似可控,但如果 src 来自 URL 参数、localStorage 或 DOM 属性,就等于把 CSP 绕过逻辑写进了业务代码里。
- 常见错误现象:
const s = document.createElement('script'); s.src = new URLSearchParams(location.search).get('callback'); - 必须前置校验:
if (!WHITELISTED_SCRIPT_SOURCES.includes(s.src)) { return; },且白名单应硬编码或由服务端下发 JSON 配置 - 避免用
eval、Function构造器或setTimeout(string)执行动态字符串代码——这些行为无法被 CSP 拦截 - 如果必须加载不可信源脚本(如插件系统),应在独立
iframe+sandbox中运行,并通过postMessage通信,隔离执行环境
最易被忽略的点:审查不是查“有没有 script”,而是查“谁决定了它的内容”。只要源头没锁死,再细的语法检查都只是擦边球。



















