| name | verify-quality-security |
| description | 安全加固——假设每个外部输入是敌意的。当涉及用户输入、认证、数据存储、上线前审查,或提到"XSS""SQL注入""CSRF""OWASP" |
Security — 安全加固
入口/出口
- 入口: 涉及用户输入、认证授权、敏感数据、或
/review 和 /ship 前
- 出口: 加固代码 + 安全审查通过
- 指向: 安全问题修复后重新进入
/review
- 前置加载: CANON.md
- 输出路径: 安全审查结论 → verify-workflow-review(合并入审查报告)或 build-workflow-execute(安全问题需退回修复时)
何时不使用
- 变更完全不接触输入、权限、敏感数据、网络边界或部署配置
- 只是解释安全概念,不审查或修改具体实现
- 当前是纯视觉/文案产物,且没有外部数据或权限边界
Iron Law
```
每个外部输入视为敌意。每个密钥视为神圣。每个鉴权检查视为强制。
安全不是"一个阶段"——安全是每行代码的约束。
安全是无条件说"不"的唯一领域。不通过安全审查 = 不能上线。
```
三级边界系统
| 级别 | 规则 | 示例 |
|---|
| Always Do | 提交前验证输入、参数化查询、哈希密码、HTTPS only cookies | 输入 sanitization、bcrypt、helmet |
| Ask First | 泄露用户数据、改权限模型、引入认证依赖、加密算法选择 | 新 OAuth provider、加密库选型 |
| Never Do | 提交密钥、信任客户端验证、eval() 用户输入、innerHTML 用户数据、暴露堆栈给用户 | .env 在仓库、dangerouslySetInnerHTML |
OWASP Top 10 防御
1. 注入 (SQL/NoSQL/Command)
const task = await db.query(`SELECT * FROM tasks WHERE id = '${req.params.id}'`);
const task = await db.tasks.findById(req.params.id);
2. 跨站脚本 (XSS)
<div>{user.bio}</div> // 如果 bio 包含 <script>...
<div>{user.bio}</div> // React 默认转义
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(richBio) }} />
3. 敏感数据暴露
catch (err) {
res.status(500).json({ error: err.message, stack: err.stack });
}
catch (err) {
logger.error('Task creation failed', { taskId, error: err.message });
res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: '处理请求时发生错误' } });
}
4. 访问控制缺失
app.delete('/tasks/:id', auth, async (req, res) => {
await db.tasks.delete(req.params.id);
});
app.delete('/tasks/:id', auth, async (req, res) => {
const task = await db.tasks.findById(req.params.id);
if (!task) return res.status(404).end();
if (task.ownerId !== req.user.id) return res.status(403).end();
await db.tasks.delete(req.params.id);
});
5. 安全配置错误
app.use(helmet());
app.use(cors({ origin: ALLOWED_ORIGINS }));
app.use(session({ cookie: { secure: true, httpOnly: true, sameSite: 'strict' } }));
6. 依赖漏洞
npm audit
npm audit fix
分类:
- Critical/High → 必须修复(修复或替换依赖)
- Moderate → 评估实际可利用性(仅在特定条件下可能利用)
- Low → 排期但不阻塞上线
输入验证
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(5000).optional(),
priority: z.enum(['low', 'medium', 'high']),
assigneeId: z.string().uuid().optional(),
});
规则: 所有外部输入在边界(API handler)验证。验证完之后内部信任。
密钥管理
项目结构:
├── .env.example # 模板(可提交)
├── .env # 真实密钥(在 .gitignore,绝不提交)
└── src/config.ts # 从环境变量读取
.gitignore 必须包含:
.env
.env.local
*.pem
*.key
credentials.json
检查是否误提交:
git log --all --full-history -- '*.env' '*.pem' 'credentials.*'
好坏示例
Good — OWASP 清单逐项验证
A01 注入:SQL 全部参数化 ✓ → A02 XSS:框架自动转义 + DOMPurify ✓ → A04 访问控制:每个写端点验证所有权 ✓ → A06 依赖:npm audit 无 high/critical ✓ → 输入验证:所有外部输入 Zod 校验 ✓。每项有通过/未通过判定。
Bad — "我们没有用户输入" 不做检查
"这只是内部工具,用户输入在前端验证过了" — 后端没有输入验证,一行 curl 即可绕过前端。SQL 拼接、未参数化查询、无权限检查。内网被攻破后横向移动无阻碍。
输出模板
安全审查结论(合并入 04-review.md 的 Security 轴区块):
## Security 审查
### OWASP Top 10 检查
| 类目 | 状态 | 发现 |
|------|------|------|
| A01 注入 | PASS / FAIL | 具体发现描述 |
| A02 XSS | PASS / FAIL | ... |
| A03 敏感数据暴露 | PASS / FAIL | ... |
| A04 访问控制缺失 | PASS / FAIL | ... |
| A05 安全配置错误 | PASS / FAIL | ... |
| A06 依赖漏洞 | PASS / FAIL | npm audit 结果摘要 |
### 输入验证覆盖
- 已验证的输入端: <列出>
- 未验证的输入端: <列出> (如有则为 Blocking)
### 密钥管理
- 密钥存储方式: 环境变量 / 配置文件 / 硬编码 (如有硬编码则为 Blocking)
- 误提交检查: git log 结果 (如有误提交则为 Blocking)
### 结论
- 审查结果: PASS / FAIL
- Blocking 问题: <列出> (如有)
- 建议: <列出>
验证证据
输出或记录必须包含:
- 输入/来源: 读取的 spec、plan、代码、反馈或发布上下文。
- 执行动作: 实际完成的检查、生成、修复、导出或发布步骤。
- 验证结果: 命令、审查结论、产物路径、截图或人工确认。
- 阻塞/回退: 未通过项、回退路径或需要 human partner 决策的问题。
常见说辞
| 说辞 | 现实 | 后果 |
|---|
| "只在内部用,不对外开放" | 内网不是信任边界。Dependency confusion、内网横向移动——内部应用仍需要安全加固。 | 内网被攻破后横向移动无阻碍,一台内网机器沦陷 = 整个内网沦陷。Target 数据泄露就是从空调供应商内网渗透的。 |
| "npm audit 报的都是低风险" | 低风险可能组合成高风险。至少读一遍 audit 报告。 | 低风险 CVE 组合利用的攻击链已经发生多次(Log4j 就是"低风险"日志库),忽略 audit = 赌博。 |
| "用户输入在前端验证过了" | 前端验证是 UX 优化,不是安全。攻击者发 curl 直接绕过。 | 一行 curl -X POST 即可绕过所有前端验证。未后端验证的输入 = SQL 注入 / XSS 的直接入口。 |
| "这是个简单的内部工具" | 简单工具暴露一个未验证输入就被用作内网跳板。 | 内部工具被钓鱼或供应链攻击攻破后成为内网跳板,损失范围从 1 个工具扩大到整个基础设施。 |
| "以后再加强安全" | 安全债和技术债不同——安全债可能导致数据泄露。不能之后补。 | 数据泄露赔偿 + 声誉损失 + 用户流失。安全事件后补救的成本是预防成本的 10-50x(IBM 安全报告)。 |
违反字面规则就是违反精神。 没有灰色地带。
验证失败处理
| 失败场景 | 处理方式 |
|---|
| 发现 SQL/命令注入(OWASP A01) | Blocking — 立即退回 build 修复,修复后重新提交安全审查,不能上线 |
| 发现 XSS / innerHTML 用户数据(OWASP A02) | Blocking — 立即退回 build 修复,必须使用参数化或 DOMPurify |
| 密钥或令牌硬编码/误提交 | Blocking — 立即移除密钥,轮换泄露密钥值,加入 .gitignore,重新提交 |
| 访问控制缺失(OWASP A04) | Blocking — 添加所有权验证或权限检查,修复后重新审查 |
| npm audit 有 Critical/High CVE | Blocking — 修复或替换依赖后才可上线,Low 可排期但不忽略 |
| 未验证输入端存在 | Blocking — 所有外部输入必须在边界验证,遗漏 = 潜在注入入口 |
红旗 — STOP
以下任何一个出现,立即停止并修复:
- 用户输入到了 SQL/Shell/HTML 中而未参数化/转义
- 密钥或令牌出现在代码或日志中
Authorization: Bearer <token> 在没有 HTTPS 的连接上
- 用
< 或 > 字符串比较做权限检查(可绕过)
- 没有速率限制的认证端点(暴力破解)
innerHTML、dangerouslySetInnerHTML、eval() 含用户数据
- API 返回的 stack trace 包含文件路径或内部库名
验证清单