返回笔记首页

第 10 集:AI Code Review 与安全扫描

主题配置

一、为什么需要 AI 安全扫描

传统 Code Review 的局限:

  • 人工 Review 依赖 Reviewer 的经验,不同人的关注点不同
  • 安全漏洞往往在不起眼的地方,容易被忽视
  • Reviewer 也会疲劳,Review 代码越多,越容易遗漏
  • 安全漏洞种类多,很难靠记忆覆盖所有类型

AI 安全扫描的优势:

  • 不会疲劳,每次扫描都全力以赴
  • 覆盖的漏洞类型比人工更全面
  • 可以集成进 CI/CD,代码合入前自动扫描
  • 能给出修复建议,不只是发现问题

二、后端常见安全漏洞类型

2.1 SQL 注入(SQL Injection)

原理:直接把用户输入拼接进 SQL 字符串,攻击者通过构造特殊输入,让 SQL 执行意外的操作。

typescript
// 有漏洞的写法
async searchUsers(keyword: string) {
  const query = `SELECT * FROM users WHERE username = '${keyword}'`;
  return await this.prisma.$queryRawUnsafe(query);
}

// 攻击者传入:' OR 1=1 --
// 实际执行的 SQL:SELECT * FROM users WHERE username = '' OR 1=1 --'
// 效果:返回数据库里所有用户

修复方式:用参数化查询,让数据库驱动处理转义

typescript
// 安全的写法一:用 Prisma ORM 查询
async searchUsers(keyword: string) {
  return await this.prisma.user.findMany({
    where: { username: { contains: keyword } },
  });
}

// 安全的写法二:用 Prisma 的参数化原生 SQL
async searchUsers(keyword: string) {
  return await this.prisma.$queryRaw`
    SELECT * FROM users WHERE username = ${keyword}
  `;
}

2.2 硬编码密钥(Hardcoded Secrets)

原理:密钥硬编码在代码里,代码一旦提交到 Git,密钥就永远留在版本历史里。即使后来删掉这行代码,git log 依然能找到历史记录。

typescript
// 有漏洞的写法
@Injectable()
export class AuthService {
    private readonly JWT_SECRET = 'super_secret_key_123' // 危险
    private readonly API_KEY = 'sk-abc123def456ghi789' // 危险
}

危害:攻击者拿到 JWT Secret,可以自己签发任意用户的 Token,绕过所有认证。

修复方式:通过环境变量读取,用 ConfigService

typescript
// 安全的写法
@Injectable()
export class AuthService {
    constructor(private configService: ConfigService) {}

    private get jwtSecret(): string {
        return this.configService.getOrThrow<string>('JWT_SECRET')
    }
}

.env 里配置:

plain
JWT_SECRET=your-actual-secret-key-here

.env.example 里记录(提交到 Git,但不填真实值):

plain
JWT_SECRET=
API_KEY=

2.3 缺少权限校验(Missing Authorization)

原理:接口只验证了"你是谁"(认证),但没有验证"你有没有权限做这件事"(授权)。

typescript
// 有漏洞的写法:任何登录用户都能删除任意其他用户
@Delete(':id')
async deleteUser(@Param('id') id: number) {
  return await this.userService.remove(id);
  // 没有检查当前操作者是否有权限删除这个 id 的用户
}

修复方式一:用 NestJS Guard 做角色校验

typescript
@Delete(':id')
@Roles(UserRole.ADMIN)   // 只有 ADMIN 角色能调用
@UseGuards(JwtAuthGuard, RolesGuard)
async deleteUser(@Param('id') id: number) {
  return await this.userService.remove(id);
}

修复方式二:用户只能操作自己的资源

typescript
@Patch(':id')
@UseGuards(JwtAuthGuard)
async updateUser(
  @Param('id') id: number,
  @CurrentUser() currentUser: User,
  @Body() dto: UpdateUserDto,
) {
  // 验证当前用户只能修改自己的信息
  if (currentUser.id !== id && currentUser.role !== UserRole.ADMIN) {
    throw new ForbiddenException('无权修改其他用户信息');
  }
  return await this.userService.update(id, dto);
}

2.4 敏感数据泄露(Sensitive Data Exposure)

原理:API 返回了不应该暴露给客户端的字段,比如密码哈希值、内部 Token、私有配置。

typescript
// 有漏洞的写法:直接返回数据库记录
async findOne(id: number) {
  return await this.prisma.user.findUnique({ where: { id } });
  // 返回了 password 字段!
}

修复方式:用 Prisma 的 select 排除敏感字段

typescript
// 安全的写法
async findOne(id: number) {
  return await this.prisma.user.findUnique({
    where: { id },
    select: {
      id: true,
      username: true,
      email: true,
      role: true,
      isActive: true,
      createdAt: true,
      // 不选 password
    },
  });
}

2.5 路径穿越(Path Traversal)

原理:文件路径拼接了用户输入,攻击者通过传入 ../ 访问系统文件。

typescript
// 有漏洞的写法
async getUserFile(userId: number, filename: string) {
  const path = `/uploads/users/${userId}/${filename}`;
  return fs.readFileSync(path, 'utf-8');
  // 攻击者传 filename = '../../etc/passwd'
  // 实际路径:/uploads/users/1/../../etc/passwd = /etc/passwd
}

修复方式:校验文件名,确保路径在预期目录内

typescript
import * as path from 'path';

async getUserFile(userId: number, filename: string) {
  // 校验文件名只包含安全字符
  if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
    throw new BadRequestException('非法文件名');
  }

  const baseDir = `/uploads/users/${userId}`;
  const filePath = path.resolve(baseDir, filename);

  // 确认解析后的路径还在 baseDir 里
  if (!filePath.startsWith(path.resolve(baseDir))) {
    throw new ForbiddenException('非法文件路径');
  }

  return fs.readFileSync(filePath, 'utf-8');
}

2.6 XSS(跨站脚本攻击)

原理:把用户输入的内容直接存入数据库,前端渲染时用 v-htmlinnerHTML 直接渲染,触发脚本执行。

后端的防御重点:

  • 不要把用户输入当作 HTML 渲染
  • 对用户输入做长度限制和内容类型校验
typescript
// 有漏洞的写法
async saveComment(userId: number, content: string) {
  // content 可能包含 <script>alert('xss')</script>
  // 直接存入数据库
  return await this.prisma.comment.create({
    data: { userId, content },
  });
}

// 修复:用 class-validator 的 @IsString() + @MaxLength() 限制内容
// 如果需要富文本,用专业的白名单过滤库(如 DOMPurify)

三、演示用的漏洞示例代码

在项目里新建 src/vulnerable-examples/vulnerable.service.ts

typescript
import { Injectable } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'
import * as fs from 'fs'
import * as path from 'path'

// ⚠️ 这是演示用的漏洞示例文件,不要在真实项目中使用

@Injectable()
export class VulnerableService {
    // 漏洞1:硬编码密钥
    private readonly JWT_SECRET = 'super_secret_key_123'
    private readonly DB_PASSWORD = 'admin123'
    private readonly API_KEY = 'sk-abc123def456ghi789'

    constructor(private prisma: PrismaService) {}

    // 漏洞2:SQL 注入
    async searchUsers(keyword: string) {
        const query = `SELECT * FROM users WHERE username = '${keyword}'`
        return await this.prisma.$queryRawUnsafe(query)
    }

    // 漏洞3:缺少权限校验
    async deleteUser(targetUserId: number) {
        return await this.prisma.user.delete({ where: { id: targetUserId } })
    }

    // 漏洞4:敏感数据泄露
    async getUserInfo(id: number) {
        return await this.prisma.user.findUnique({ where: { id } })
    }

    // 漏洞5:路径穿越
    async readUserFile(userId: number, filename: string) {
        const filePath = `/uploads/users/${userId}/${filename}`
        return fs.readFileSync(filePath, 'utf-8')
    }

    // 漏洞6:没有速率限制的密码验证(暴力破解风险)
    async verifyPassword(userId: number, password: string) {
        const user = await this.prisma.user.findUnique({
            where: { id: userId },
        })
        return user?.password === password
    }
}

四、PR 模板和 Code Review Checklist

.github/pull_request_template.md 里写入:

markdown
## PR 描述

### 变更内容

### 变更类型

- [ ] 新功能
- [ ] Bug 修复
- [ ] 重构
- [ ] 文档更新

### 测试情况

- [ ] 已添加单元测试
- [ ] 已在本地运行测试,全部通过
- [ ] 覆盖率没有下降

### AI Code Review Checklist(合入前必须完成)

- [ ] 已用 AI 扫描安全漏洞(SQL 注入、XSS、硬编码 Key、权限缺失、敏感字段暴露)
- [ ] AI 发现的问题已全部修复,或已说明为何可接受
- [ ] 没有 console.log 或调试代码残留
- [ ] 没有 any 类型(或已有书面说明为何不得不用)
- [ ] 密码等敏感字段不在 API 响应里出现

### AI Review 结果摘要

五、CI/CD 集成安全扫描

.github/workflows/security-scan.yml 里写入:

yaml
name: Security Scan

on:
    pull_request:
        branches: [main, develop]

jobs:
    security:
        runs-on: ubuntu-latest

        steps:
            - uses: actions/checkout@v4

            - name: Setup Node.js
              uses: actions/setup-node@v4
              with:
                  node-version: '20'

            - name: Install dependencies
              run: npm ci

            - name: TypeScript check
              run: npx tsc --noEmit

            - name: Run tests
              run: npm test -- --coverage

            - name: Install Claude Code
              run: npm install -g @anthropic-ai/claude-code

            - name: AI Security Scan
              env:
                  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
              run: |
                  claude "扫描 src/ 目录下所有 .ts 文件,检查以下安全问题:
                  1. SQL 注入:是否有字符串拼接 SQL 的写法($queryRawUnsafe + 变量拼接)
                  2. 硬编码密钥:是否有明文的 key、secret、password、token 赋值
                  3. 权限缺失:DELETE/PATCH 接口是否有 @Roles 或权限校验
                  4. 敏感字段暴露:findUnique/findMany 是否直接返回(未排除 password)
                  5. 路径穿越:是否有文件路径拼接了用户输入

                  把扫描结果写入 security-report.txt,
                  如果没有发现问题,写入内容:SCAN PASSED - No security issues found"

            - name: Check result
              run: |
                  cat security-report.txt
                  grep -q "SCAN PASSED" security-report.txt || exit 1

            - name: Upload report
              uses: actions/upload-artifact@v4
              if: always()
              with:
                  name: security-report
                  path: security-report.txt

配置 GitHub Secrets

plain
仓库 Settings → Secrets and variables → Actions → New repository secret
Name: ANTHROPIC_API_KEY
Value: 你的 Anthropic API Key

六、演示操作步骤

准备工作

Step 1:创建目录和漏洞示例文件

bash
mkdir -p src/vulnerable-examples
mkdir -p .github/workflows

把第三章的漏洞代码写入 src/vulnerable-examples/vulnerable.service.ts

Step 2:创建 PR 模板

bash
mkdir -p .github

把第四章的 PR 模板写入 .github/pull_request_template.md


安全扫描演示

Step 1:在 Cursor Chat 里输入:

plain
@src/vulnerable-examples/vulnerable.service.ts

对这个文件做完整的安全审查,按以下维度检查:

1. 注入类漏洞:SQL 注入、命令注入、路径穿越
2. 认证/授权:缺少权限校验、暴力破解风险
3. 数据泄露:敏感字段暴露、硬编码密钥
4. 输入处理:XSS 风险、未校验的用户输入

对每个发现的问题,按以下格式输出:
### 漏洞 [编号]:[漏洞名称]
- 危险等级:高 / 中 / 低
- 位置:第几行,哪个方法
- 攻击方式:攻击者如何利用这个漏洞
- 修复建议:[提供修复后的代码]

Step 2:检查 AI 是否发现了所有 6 个漏洞

如果漏掉了某个,追加提问:

plain
你的报告里没有提到 readUserFile 方法,这个方法有路径穿越漏洞,
请分析并给出修复方案

修复演示

修复 SQL 注入

在 Chat 里输入:

plain
修复 searchUsers 方法的 SQL 注入漏洞,
把 $queryRawUnsafe 改成用 Prisma 的 where 条件查询
修复硬编码密钥

在 Chat 里输入:

plain
修复硬编码密钥问题:
1. 注入 ConfigService
2. JWT_SECRET 改从 this.configService.getOrThrow('JWT_SECRET') 读取
3. API_KEY 改从 this.configService.getOrThrow('API_KEY') 读取
4. 在项目根目录的 .env.example 文件里加入这两个配置项(空值)
验证修复结果

修复完成后,重新让 AI 扫描:

plain
@src/vulnerable-examples/vulnerable.service.ts

重新扫描这个文件,确认 SQL 注入和硬编码密钥的漏洞是否已经修复,
还有没有其他遗留的安全问题

CI 配置演示

Step 1:创建 GitHub Actions 配置文件

把第五章的 CI 配置写入 .github/workflows/security-scan.yml

Step 2:展示文件结构

bash
ls -la .github/
ls -la .github/workflows/
cat .github/workflows/security-scan.yml

Step 3:解释每个步骤的作用

CI 步骤 作用
TypeScript check 编译报错直接失败,不进行后续步骤
Run tests 测试不通过,不允许合入
AI Security Scan 发现安全漏洞,写入报告文件
Check result 读取报告,没有 SCAN PASSED 就让 CI 失败
Upload report 上传报告文件,Review 者可以下载查看详情

Step 4:说明 GitHub Secrets 配置方式(不要在演示时展示真实的 API Key)

plain
配置位置:
GitHub 仓库页面 → Settings → Secrets and variables → Actions
→ New repository secret

Name: ANTHROPIC_API_KEY
Value: 从 platform.anthropic.com 获取的 API Key

七、安全扫描的局限性

AI 安全扫描不能替代完整的安全审计,它的局限:

  • 无法发现业务逻辑层面的安全问题(比如权限模型设计缺陷)
  • 无法发现依赖包的安全漏洞(用 npm audit 补充)
  • 对于混淆或者复杂的攻击向量可能漏掉
  • 误报率有一定比例,发现的问题需要人工确认

完整的安全防线应该是

plain
AI 安全扫描(快速、覆盖常见漏洞)
    +
npm audit(依赖包漏洞扫描)
    +
人工 Code Review(业务逻辑安全)
    +
定期的专业安全审计(复杂项目)

Spec Coding 实战补充:10 AI Code Review 与安全扫描

来源:Spec Coding实战/10 AI Code Review 与安全扫描.md,已合并到本章节。

1. AI Code Review 能做什么

传统 Code Review 依赖人工,有几个固有的问题:

  • 审查质量取决于审查者的经验和当天的状态
  • 紧急上线时容易草草了事
  • 同一类问题反复出现,靠人工一遍遍提
  • 跨时区团队等 Review 会阻塞开发进度

AI Code Review 不是要替代人工 Review,而是在人工 Review 之前,先做一轮机械化、系统化的检查,把常见问题过滤掉,让人工 Review 聚焦在更高层次的设计讨论上。

AI 擅长的 Review 维度

  • 安全漏洞(SQL 注入、XSS、硬编码密钥)
  • 潜在的 bug(边界条件、空指针、类型错误)
  • 代码风格和命名
  • 性能问题(N+1 查询、缺少索引提示)
  • 遗漏的错误处理
AI 不擅长的 Review 维度
  • 业务逻辑是否正确(AI 不了解你的业务)
  • 架构设计是否合理
  • 是否满足产品需求

2. 标准 Code Review Prompt 模板

通用 Review

plain
请 review @src/modules/order/order.service.ts,重点检查:

【安全】
- 是否存在 SQL 注入(直接拼接用户输入到 SQL)
- 是否存在硬编码的密钥、密码、API Key
- 敏感字段(password、token)是否可能出现在响应或日志中
- 权限校验是否完整(用户能否访问不属于自己的数据)

【正确性】
- 是否有未处理的 Promise(没有 await 或 .catch)
- 是否有空指针风险(访问可能为 null/undefined 的属性)
- 数字计算是否有精度问题(金额计算不应用浮点数)
- 边界条件是否处理(空数组、零值、极大值)

【性能】
- 是否有 N+1 查询(循环内查数据库)
- 频繁查询的接口是否缺少缓存
- 大数据量查询是否有分页保护

【可维护性】
- 是否有魔法数字/字符串(应该定义为常量)
- 函数是否过长(超过 30 行需要说明理由)
- 命名是否清晰

对每个问题:指出具体行号,说明为什么有问题,给出修复建议。

3. 安全扫描详解

3.1 SQL 注入扫描

plain
扫描 @src/ 目录下所有文件,找出所有可能存在 SQL 注入风险的代码。
特征:将用户输入直接拼接进 SQL 字符串,或使用模板字符串构建查询。

报告格式:
- 文件路径 + 行号
- 有问题的代码片段
- 修复方案
有问题的代码示例
typescript
// ❌ SQL 注入风险
async searchUsers(keyword: string) {
  return this.prisma.$queryRaw`
    SELECT * FROM users WHERE name LIKE '%${keyword}%'
  `
}

// ❌ 更隐蔽的拼接方式
const query = `SELECT * FROM users WHERE id = '${userId}'`
await this.db.query(query)
修复后
typescript
// ✅ 参数化查询
async searchUsers(keyword: string) {
  return this.prisma.user.findMany({
    where: {
      name: { contains: keyword, mode: 'insensitive' },
    },
  })
}

// ✅ Prisma $queryRaw 的安全用法(自动转义)
async searchUsers(keyword: string) {
  return this.prisma.$queryRaw`
    SELECT * FROM users WHERE name ILIKE ${'%' + keyword + '%'}
  `
}

3.2 硬编码密钥扫描

plain
扫描 @src/ 目录下所有 .ts 文件,找出所有硬编码的敏感信息:
- JWT secret
- 数据库连接字符串
- API Key(包含 "key", "secret", "token", "password" 的字符串字面量)
- 私钥、证书内容

排除:
- 测试文件(.spec.ts)中的 Mock 值
- 注释里的示例值
有问题的代码
typescript
// ❌ 硬编码 secret
const token = jwt.sign(payload, 'my-super-secret-key-123')

// ❌ 硬编码数据库密码
const db = new Pool({ password: 'postgres123' })

// ❌ 硬编码 API Key
const headers = { 'X-API-Key': 'sk-1234567890abcdef' }
修复后
typescript
// ✅ 从配置读取
const token = jwt.sign(payload, this.configService.get('JWT_SECRET'))
const db = new Pool({ password: process.env.DB_PASSWORD })
const headers = { 'X-API-Key': this.configService.get('EXTERNAL_API_KEY') }

3.3 XSS 扫描(针对有前端渲染的场景)

plain
扫描代码中所有直接将用户输入渲染到 HTML 的地方:
- innerHTML 赋值
- v-html 指令(Vue)
- dangerouslySetInnerHTML(React)
- 未转义的模板字符串输出到 HTML

对于每个发现的地方,说明是否有 XSS 风险,以及如何修复或消毒。

3.4 权限绕过扫描

plain
检查 @src/modules/ 下所有 Controller,确认:

1. 每个需要认证的接口是否都加了 @UseGuards(JwtAuthGuard)
   (或等效的认证装饰器)

2. 涉及用户私有数据的接口(如 GET /users/:id,PATCH /users/:id)
   是否校验了当前登录用户只能操作自己的数据

3. 有没有接口遗漏了权限校验,任何人都能访问

列出所有有问题的接口,给出补充权限校验的代码。
典型漏洞
typescript
// ❌ 没有校验 userId 归属
@Patch(':id')
async update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
  return this.userService.update(id, dto)  // 任何登录用户都能修改其他用户
}

// ✅ 校验归属
@Patch(':id')
async update(
  @Param('id') id: string,
  @Body() dto: UpdateUserDto,
  @CurrentUser() user: JwtPayload,
) {
  if (id !== user.sub && user.role !== 'ADMIN') {
    throw new BusinessException(ErrorCode.FORBIDDEN)
  }
  return this.userService.update(id, dto)
}

4. 自动化 Code Review 脚本

把 Review 流程集成到提交前的自动化检查里:

方式一:Git Pre-push Hook

bash
# .git/hooks/pre-push(或用 husky 管理)
#!/bin/sh

echo "Running AI security scan..."

# 获取本次推送涉及的变更文件
changed_files=$(git diff --name-only origin/main...HEAD -- '*.ts' | grep -v '.spec.ts')

if [ -n "$changed_files" ]; then
  echo "$changed_files" | xargs cat | claude "
    快速安全扫描,只报告高风险问题:
    1. SQL 注入
    2. 硬编码密钥
    3. 未经认证的敏感接口

    如果没有发现问题,只输出:PASS
    如果发现问题,输出:FAIL + 具体说明
  "
fi

方式二:Claude Code 集成到 CI

yaml
# .github/workflows/ai-review.yml
name: AI Code Review

on:
    pull_request:
        branches: [main]

jobs:
    ai-review:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4
              with:
                  fetch-depth: 0

            - name: Install Claude Code
              run: npm install -g @anthropic-ai/claude-code

            - name: Run Security Scan
              env:
                  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
              run: |
                  git diff origin/main...HEAD -- '*.ts' | claude "
                    对这个 PR 的改动做安全扫描,检查:
                    No SQL Injection(无 SQL 注入)
                    No Hardcoded Secrets(无硬编码密钥)
                    No Missing Auth(无遗漏认证)

                    按 JSON 格式返回:
                    { 'passed': boolean, 'issues': [{ 'severity': 'high|medium|low', 'file': '', 'line': '', 'description': '' }] }
                  " > review-result.json

                  # 如果有 high severity 问题,让 CI 失败
                  python3 -c "
                  import json, sys
                  result = json.load(open('review-result.json'))
                  high_issues = [i for i in result['issues'] if i['severity'] == 'high']
                  if high_issues:
                      for issue in high_issues:
                          print(f'HIGH: {issue[\"file\"]}:{issue[\"line\"]} - {issue[\"description\"]}')
                      sys.exit(1)
                  "

5. 针对 PR 的 Review

在提 PR 前,用 Claude Code 做一次完整 Review:

bash
# 查看本次 PR 的所有改动
git diff main...HEAD | claude "
作为高级后端工程师,review 这个 PR 的改动:

**必须检查(有问题必须改):**
- 安全漏洞:SQL注入、XSS、硬编码密钥、权限绕过
- 数据一致性:需要事务但没有事务的操作
- 潜在崩溃:空指针、未处理的 Promise rejection

**建议检查(可以忽略但最好改):**
- 性能:N+1查询、缺少必要索引
- 可读性:命名、注释、函数过长

**不需要检查:**
- 代码风格(有 Prettier 保证)
- TypeScript 类型(有 tsc 保证)

输出格式:
## 必须修复
[问题列表,每条包含文件名、行号、问题描述、修复建议]

## 建议优化
[同上]

## 总体评价
[一段简短的 PR 质量评价]
"

6. Review 结果的处理

AI 给出 Review 意见后,不是所有问题都需要修复。

处理优先级

级别 示例 处理方式
必须修复 SQL 注入、硬编码密钥、未认证接口 合并前必须解决
应该修复 N+1 查询、缺少错误处理 本 PR 修或开新 ticket
建议改进 命名可以更清晰、函数可以拆分 根据时间决定
忽略 风格偏好 不处理

对于 AI 给出但你不认同的意见,可以继续追问:

plain
你说第 45 行有 N+1 查询问题,但这个接口预期的数据量很小(最多 10 条),
而且有缓存,你认为这种情况下还需要优化吗?

AI 会给出更有针对性的分析,帮你做出合理判断。


7. 小结

AI Code Review 的核心价值是一致性和覆盖率:不会因为时间紧就跳过,不会漏掉某类问题,不依赖个人经验。

落地建议

  • 个人开发:每次提 PR 前跑一次 git diff main...HEAD | claude "review..."
  • 小团队:把 AI Review 集成到 PR 流程,作为 Bot 评论
  • 大团队:在 CI 里加安全扫描,高风险问题自动阻断合并

记住一个原则:AI Review 发现的问题不等于你的代码一定有问题,发现不了的问题也不等于代码没问题。 AI 是辅助工具,最终判断还是你来做。