一、CSP为什么是React防XSS的关键防线
React的JSX默认会对插入的变量进行转义,例如在渲染用户输入时,尖括号会被转换为实体,所以很多简单的反射型XSS在React中难以直接生效。但这并不意味着React应用就天然免疫XSS。当开发者使用 dangerouslySetInnerHTML 属性渲染富文本、或者引入的第三方库存在漏洞、又或者攻击者通过其他方式向页面注入了脚本标签,React本身无法完全阻止这些脚本的执行。浏览器一旦解析到恶意脚本,就会在应用的源站上下文中运行,窃取Cookie、调用API甚至篡改页面内容。

内容安全策略(Content Security Policy,CSP)正是一种在浏览器端执行的防护机制。它通过HTTP响应头告诉浏览器,当前页面允许从哪些源加载脚本、样式、图片、字体等资源,以及是否允许内联脚本执行。当浏览器检测到某个资源请求违反了策略时,会直接拦截该请求,并且默认情况下不加载对应资源。这就从源头上切断了恶意脚本的执行路径。对于React应用来说,CSP可以限制脚本只能来自自身域名或受信任的CDN,即使攻击者成功注入了脚本标签,只要该脚本的来源不在白名单内,浏览器也会拒绝执行。
二、React项目中如何设置CSP响应头
在React开发环境中,CSP通常不会默认开启,因为开发服务器需要热更新和错误覆盖层等内联脚本。但在生产环境,强烈建议通过Web服务器或反向代理来添加响应头。以下以Nginx为例,在配置文件的server块中加入add_header指令:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self';";
上述策略表示默认只允许同源资源,脚本只允许同源,样式允许同源和内联,图片允许同源和data协议,连接请求只允许同源。其中script-src没有包含unsafe-inline,意味着内联脚本将被禁止。React构建产物中通常会生成一个runtime的script标签直接嵌入HTML,如果直接部署这样的配置,应用可能无法启动。这是因为Create React App的构建结果在index.html中会有一小段内联脚本用于引导加载bundle。为了解决这个矛盾,可以通过nonce或hash的方式动态允许脚本。
另一种方式是在Node.js的Express服务中设置响应头。例如:
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self';"
);
next();
});
无论使用哪种服务器,核心都是输出正确的Content-Security-Policy头。需要注意的是,一旦设置了CSP,浏览器会严格执行,如果策略过严,会阻止正常功能;如果过松,又失去了防护意义。因此需要结合React项目的实际资源引用情况来调整指令。
三、用nonce和hash解决React内联脚本问题
前面提到,React生产构建往往会在HTML中直接嵌入一小段初始化脚本,CSP的script-src默认禁止unsafe-inline会让这些内联脚本无法执行。为了让内联脚本合规,CSP提供了两种机制:nonce(随机数)和hash(哈希)。nonce是在服务器端生成一个随机字符串,每次响应都不同,并将该字符串同时放入CSP头和script标签的nonce属性中。浏览器只放行nonce值匹配的内联脚本。例如在Express中可以这样实现:
const crypto = require('crypto');
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
`script-src 'self' 'nonce-${nonce}'; style-src 'self' 'unsafe-inline';`
);
next();
});
然后在返回HTML时,需要把内联script标签添加上nonce属性。对于React构建产生的index.html,可以通过模板字符串替换占位符的方式实现。例如将构建产物中的<script>标签统一替换为带有nonce的形式。但这种方式操作起来比较繁琐,而且每次请求nonce都会变化,需要确保HTML中的nonce与响应头匹配。
另一种更简单的方案是使用hash。CSP允许你计算内联脚本内容的哈希值(如SHA-256),然后将该哈希加入到script-src指令中。浏览器会重新计算脚本的哈希并和策略中的值比对,如果一致则允许执行。例如对于一段固定的内联脚本,可以生成类似script-src 'sha256-xxxxxx'的策略。但React构建的内联脚本内容通常包含运行时变量,哈希值在每次构建后是固定的,只要不修改构建产物,就可以提前计算并写入CSP头。然而如果构建文件更新,哈希值也会改变,需要同步更新策略。因此nonce更适合动态场景,hash更适合静态资源。
四、CSP在React环境中的常见误区与调试技巧
配置CSP时最常见的错误是直接复制网上的策略,包含unsafe-inline或unsafe-eval。unsafe-inline会允许所有内联脚本,基本让CSP形同虚设;unsafe-eval则允许eval函数,对React应用来说通常是不需要的,除非你使用了某些依赖eval的库。如果开发环境依赖热更新,可以在开发服务器上放宽策略,但在生产环境一定要收紧。
另外,一些第三方服务需要额外放宽指令。比如使用Google Analytics或地图服务,需要将对应的域名加入script-src和img-src。很多开发者容易漏掉connect-src,导致API请求被拦截。React应用通常通过fetch或axios调用后端接口,这些请求受connect-src控制,如果后端域名与前端不同,必须显式加入白名单。
调试CSP策略时,可以暂时使用Content-Security-Policy-Report-Only头,让浏览器只报告违规而不实际拦截。同时设置report-uri或report-to指令把违规报告发送到一个端点,方便收集错误。浏览器开发者工具的控制台也会输出CSP违规信息,包含被拦截的具体资源和违反的指令。通过这些提示可以逐步调整策略,直到应用功能完全正常。最后再把报告模式切换为强制模式,确保安全策略真正生效。