在CDN普及的今天,几乎没有哪个页面的JavaScript是全部自己写的。统计、广告、埋点、客服组件、开源框架,大量脚本来自第三方域名。问题在于:这些脚本一旦在传输链路中被劫持或替换,你的页面就会执行攻击者的代码,而且是以你站点的身份执行,能读Cookie、能发请求、能改DOM。验证JavaScript代码的完整性,本质上是给每一段要执行的脚本加上身份校验。下面从浏览器原生支持的几个机制入手,讲清楚具体怎么做。

一、Subresource Integrity:用哈希值锁定脚本内容
Subresource Integrity,中文常译为子资源完整性,简称SRI。它的原理很直接:你在引入外部脚本时,附上该脚本内容的加密哈希值,浏览器下载脚本后会重新计算哈希,与标签中声明的值比对,不一致就拒绝执行。
具体用法是给<script>标签加上integrity属性,值由算法名和Base64编码的哈希组成:
<script src="https://cdn.ipipp.com/lib/vue/3.4.0/vue.global.prod.js"
integrity="sha384-6mY5eZtGcGz6Xv7XKqX4Y2NqXW8qf9YqZ6X9X2F5T5V5y4U3I6P7Q2R1S8T9"
crossorigin="anonymous"></script>这里有三个细节需要注意。第一,integrity支持sha384、sha512,也兼容sha256,推荐使用sha384或更高,破解成本足够高。第二,可以同时写多个算法的哈希,用空格分隔,浏览器只要匹配任意一个即可,方便算法迁移。第三,crossorigin属性是必须的,即使你的CDN没有配置CORS头,也需要写crossorigin="anonymous",否则SRI校验会被跳过,等于白加。
SRI最大的坑在于版本更新。脚本内容一变,哈希就变,页面会直接拒绝加载新版本。所以SRI适合锁定具体版本号的资源,对于那些自动升级的latest链接,反而会造成维护负担。工程上常见的做法是在构建阶段自动计算哈希并注入到HTML模板里,比如用webpack的插件或sri-stats工具,避免手工维护。
另一个容易忽略的场景是preload。如果你用<link rel="preload">预加载脚本,preload标签上同样要带integrity和crossorigin属性,否则预加载的资源无法被SRI校验复用,浏览器会重新下载一份,甚至控制台直接报错。
二、CSP:从源头限制脚本来源
SRI解决的是内容对不对的问题,CSP(Content Security Policy)解决的是来源对不对的问题。两者配合使用,防护才是完整的。CSP通过HTTP响应头或meta标签下发策略,其中与脚本直接相关的是script-src指令。
一个典型的配置如下:
<!-- 通过HTTP头下发更安全,meta方式作为兜底 -->
Content-Security-Policy:
script-src 'self'
https://cdn.ipipp.com
'nonce-r4nd0mStr1ng'
'strict-dynamic';
object-src 'none';
base-uri 'self'这条策略的含义是:脚本只能来自本站和指定的CDN域名,或者携带正确nonce值的内联脚本。nonce是一次性随机字符串,每次响应都不同,攻击者即使注入了脚本标签,猜不出nonce就无法执行。strict-dynamic则允许被nonce授权的脚本再去动态加载其他脚本,解决了第三方SDK内部动态创建script标签的问题。
这里要强调nonce和哈希的取舍。如果你能控制服务器每次动态生成响应,nonce最灵活;如果页面是静态托管的,用hash替代nonce,把可信内联脚本的内容哈希写进策略即可。两者不建议混用,strict-dynamic会让白名单域名失效,只信任nonce或hash授权的脚本,这是刻意为之的设计,因为域名白名单历史上被绕过的案例太多了。
上线CSP前务必先用Content-Security-Policy-Report-Only头跑一段时间,配合report-uri收集违规上报,确认没有误伤正常业务再强制开启。一次性全量上线CSP导致页面白屏的事故并不少见。
三、动态加载脚本的校验与监控兜底
业务代码里经常有动态加载脚本的场景,比如按需加载的SDK、延迟初始化的组件。浏览器对动态创建的script标签同样会校验integrity属性,所以代码里也要带上:
function loadScript(url, integrity) {
return new Promise((resolve, reject) => {
const s = document.createElement('script');
s.src = url;
s.integrity = integrity;
s.crossOrigin = 'anonymous';
s.onload = resolve;
s.onerror = () => reject(new Error('脚本加载或校验失败: ' + url));
document.head.appendChild(s);
});
}
// 校验失败时会触发onerror,可以在这里上报并降级
loadScript('https://cdn.ipipp.com/sdk/track-1.2.3.js',
'sha384-Base64EncodedHashValueHere')
.catch(err => {
// 上报到监控平台,必要时切换备用CDN
reportError(err);
return loadScript('https://backup.ipipp.com/sdk/track-1.2.3.js',
'sha384-Base64EncodedHashValueHere');
});onerror无法区分是网络失败还是校验失败,但无论哪种情况,切换备用源重试都是合理策略。关键是监控平台要能区分场景:如果大量用户出现特定脚本校验失败,很可能是链路劫持或CDN被入侵,需要立即响应。
最后是整体防护的层次问题。传输层强制HTTPS和HSTS是前提,否则中间人可以直接把带integrity的script标签连同整个HTML一起改掉,SRI就形同虚设。SRI锁定内容、CSP锁定来源、HTTPS锁定通道、监控提供事后追溯,四层叠加,JavaScript代码的完整性验证才算真正落地。对于金融、政务这类高安全要求的项目,还可以考虑在构建流水线中对所有静态资源做签名清单,页面加载时用WebAssembly模块本地核验清单签名,进一步收窄信任边界。
实践建议按顺序推进:先上HTTPS和HSTS,再给固定的第三方脚本补齐integrity属性,然后灰度开启CSP的Report-Only观察,最后强制执行并接入监控告警。每一步都可独立验证效果,风险可控,也不需要一次性推翻现有架构。
Subresource IntegrityCSP前端安全修改时间:2026-09-12 17:18:44