一、为什么需要 AI 安全扫描
传统 Code Review 的局限:
- 人工 Review 依赖 Reviewer 的经验,不同人的关注点不同
- 安全漏洞往往在不起眼的地方,容易被忽视
- Reviewer 也会疲劳,Review 代码越多,越容易遗漏
- 安全漏洞种类多,很难靠记忆覆盖所有类型
AI 安全扫描的优势:
- 不会疲劳,每次扫描都全力以赴
- 覆盖的漏洞类型比人工更全面
- 可以集成进 CI/CD,代码合入前自动扫描
- 能给出修复建议,不只是发现问题
二、后端常见安全漏洞类型
2.1 SQL 注入(SQL Injection)
原理:直接把用户输入拼接进 SQL 字符串,攻击者通过构造特殊输入,让 SQL 执行意外的操作。
// 有漏洞的写法
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 --'
// 效果:返回数据库里所有用户
修复方式:用参数化查询,让数据库驱动处理转义
// 安全的写法一:用 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 依然能找到历史记录。
// 有漏洞的写法
@Injectable()
export class AuthService {
private readonly JWT_SECRET = 'super_secret_key_123' // 危险
private readonly API_KEY = 'sk-abc123def456ghi789' // 危险
}
危害:攻击者拿到 JWT Secret,可以自己签发任意用户的 Token,绕过所有认证。
修复方式:通过环境变量读取,用 ConfigService
// 安全的写法
@Injectable()
export class AuthService {
constructor(private configService: ConfigService) {}
private get jwtSecret(): string {
return this.configService.getOrThrow<string>('JWT_SECRET')
}
}
在 .env 里配置:
JWT_SECRET=your-actual-secret-key-here
在 .env.example 里记录(提交到 Git,但不填真实值):
JWT_SECRET=
API_KEY=
2.3 缺少权限校验(Missing Authorization)
原理:接口只验证了"你是谁"(认证),但没有验证"你有没有权限做这件事"(授权)。
// 有漏洞的写法:任何登录用户都能删除任意其他用户
@Delete(':id')
async deleteUser(@Param('id') id: number) {
return await this.userService.remove(id);
// 没有检查当前操作者是否有权限删除这个 id 的用户
}
修复方式一:用 NestJS Guard 做角色校验
@Delete(':id')
@Roles(UserRole.ADMIN) // 只有 ADMIN 角色能调用
@UseGuards(JwtAuthGuard, RolesGuard)
async deleteUser(@Param('id') id: number) {
return await this.userService.remove(id);
}
修复方式二:用户只能操作自己的资源
@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、私有配置。
// 有漏洞的写法:直接返回数据库记录
async findOne(id: number) {
return await this.prisma.user.findUnique({ where: { id } });
// 返回了 password 字段!
}
修复方式:用 Prisma 的 select 排除敏感字段
// 安全的写法
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)
原理:文件路径拼接了用户输入,攻击者通过传入 ../ 访问系统文件。
// 有漏洞的写法
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
}
修复方式:校验文件名,确保路径在预期目录内
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-html 或 innerHTML 直接渲染,触发脚本执行。
后端的防御重点:
- 不要把用户输入当作 HTML 渲染
- 对用户输入做长度限制和内容类型校验
// 有漏洞的写法
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:
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 里写入:
## PR 描述
### 变更内容
### 变更类型
- [ ] 新功能
- [ ] Bug 修复
- [ ] 重构
- [ ] 文档更新
### 测试情况
- [ ] 已添加单元测试
- [ ] 已在本地运行测试,全部通过
- [ ] 覆盖率没有下降
### AI Code Review Checklist(合入前必须完成)
- [ ] 已用 AI 扫描安全漏洞(SQL 注入、XSS、硬编码 Key、权限缺失、敏感字段暴露)
- [ ] AI 发现的问题已全部修复,或已说明为何可接受
- [ ] 没有 console.log 或调试代码残留
- [ ] 没有 any 类型(或已有书面说明为何不得不用)
- [ ] 密码等敏感字段不在 API 响应里出现
### AI Review 结果摘要
五、CI/CD 集成安全扫描
在 .github/workflows/security-scan.yml 里写入:
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
仓库 Settings → Secrets and variables → Actions → New repository secret
Name: ANTHROPIC_API_KEY
Value: 你的 Anthropic API Key
六、演示操作步骤
准备工作
Step 1:创建目录和漏洞示例文件
mkdir -p src/vulnerable-examples
mkdir -p .github/workflows
把第三章的漏洞代码写入 src/vulnerable-examples/vulnerable.service.ts。
Step 2:创建 PR 模板
mkdir -p .github
把第四章的 PR 模板写入 .github/pull_request_template.md。
安全扫描演示
Step 1:在 Cursor Chat 里输入:
@src/vulnerable-examples/vulnerable.service.ts
对这个文件做完整的安全审查,按以下维度检查:
1. 注入类漏洞:SQL 注入、命令注入、路径穿越
2. 认证/授权:缺少权限校验、暴力破解风险
3. 数据泄露:敏感字段暴露、硬编码密钥
4. 输入处理:XSS 风险、未校验的用户输入
对每个发现的问题,按以下格式输出:
### 漏洞 [编号]:[漏洞名称]
- 危险等级:高 / 中 / 低
- 位置:第几行,哪个方法
- 攻击方式:攻击者如何利用这个漏洞
- 修复建议:[提供修复后的代码]
Step 2:检查 AI 是否发现了所有 6 个漏洞
如果漏掉了某个,追加提问:
你的报告里没有提到 readUserFile 方法,这个方法有路径穿越漏洞,
请分析并给出修复方案
修复演示
修复 SQL 注入
在 Chat 里输入:
修复 searchUsers 方法的 SQL 注入漏洞,
把 $queryRawUnsafe 改成用 Prisma 的 where 条件查询
修复硬编码密钥
在 Chat 里输入:
修复硬编码密钥问题:
1. 注入 ConfigService
2. JWT_SECRET 改从 this.configService.getOrThrow('JWT_SECRET') 读取
3. API_KEY 改从 this.configService.getOrThrow('API_KEY') 读取
4. 在项目根目录的 .env.example 文件里加入这两个配置项(空值)
验证修复结果
修复完成后,重新让 AI 扫描:
@src/vulnerable-examples/vulnerable.service.ts
重新扫描这个文件,确认 SQL 注入和硬编码密钥的漏洞是否已经修复,
还有没有其他遗留的安全问题
CI 配置演示
Step 1:创建 GitHub Actions 配置文件
把第五章的 CI 配置写入 .github/workflows/security-scan.yml。
Step 2:展示文件结构
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)
配置位置:
GitHub 仓库页面 → Settings → Secrets and variables → Actions
→ New repository secret
Name: ANTHROPIC_API_KEY
Value: 从 platform.anthropic.com 获取的 API Key
七、安全扫描的局限性
AI 安全扫描不能替代完整的安全审计,它的局限:
- 无法发现业务逻辑层面的安全问题(比如权限模型设计缺陷)
- 无法发现依赖包的安全漏洞(用
npm audit补充) - 对于混淆或者复杂的攻击向量可能漏掉
- 误报率有一定比例,发现的问题需要人工确认
完整的安全防线应该是
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
请 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 注入扫描
扫描 @src/ 目录下所有文件,找出所有可能存在 SQL 注入风险的代码。
特征:将用户输入直接拼接进 SQL 字符串,或使用模板字符串构建查询。
报告格式:
- 文件路径 + 行号
- 有问题的代码片段
- 修复方案
有问题的代码示例
// ❌ 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)
修复后
// ✅ 参数化查询
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 硬编码密钥扫描
扫描 @src/ 目录下所有 .ts 文件,找出所有硬编码的敏感信息:
- JWT secret
- 数据库连接字符串
- API Key(包含 "key", "secret", "token", "password" 的字符串字面量)
- 私钥、证书内容
排除:
- 测试文件(.spec.ts)中的 Mock 值
- 注释里的示例值
有问题的代码
// ❌ 硬编码 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' }
修复后
// ✅ 从配置读取
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 扫描(针对有前端渲染的场景)
扫描代码中所有直接将用户输入渲染到 HTML 的地方:
- innerHTML 赋值
- v-html 指令(Vue)
- dangerouslySetInnerHTML(React)
- 未转义的模板字符串输出到 HTML
对于每个发现的地方,说明是否有 XSS 风险,以及如何修复或消毒。
3.4 权限绕过扫描
检查 @src/modules/ 下所有 Controller,确认:
1. 每个需要认证的接口是否都加了 @UseGuards(JwtAuthGuard)
(或等效的认证装饰器)
2. 涉及用户私有数据的接口(如 GET /users/:id,PATCH /users/:id)
是否校验了当前登录用户只能操作自己的数据
3. 有没有接口遗漏了权限校验,任何人都能访问
列出所有有问题的接口,给出补充权限校验的代码。
典型漏洞
// ❌ 没有校验 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
# .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
# .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:
# 查看本次 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 给出但你不认同的意见,可以继续追问:
你说第 45 行有 N+1 查询问题,但这个接口预期的数据量很小(最多 10 条),
而且有缓存,你认为这种情况下还需要优化吗?
AI 会给出更有针对性的分析,帮你做出合理判断。
7. 小结
AI Code Review 的核心价值是一致性和覆盖率:不会因为时间紧就跳过,不会漏掉某类问题,不依赖个人经验。
落地建议
- 个人开发:每次提 PR 前跑一次
git diff main...HEAD | claude "review..." - 小团队:把 AI Review 集成到 PR 流程,作为 Bot 评论
- 大团队:在 CI 里加安全扫描,高风险问题自动阻断合并
记住一个原则:AI Review 发现的问题不等于你的代码一定有问题,发现不了的问题也不等于代码没问题。 AI 是辅助工具,最终判断还是你来做。