Entry/Exit
- Entry: involves user input, authentication and authorization, sensitive data, or
/reviewand/shipbefore - Exit: hardened code + security review passed
- Points to: after security issues are fixed, re-enter
/review - Preload: CANON.md
- Output path: security review conclusion → verify-workflow-review (merged into the review report) or build-workflow-execute (when security issues need to be sent back for fixing)
When not to use
- The change does not touch input, permissions, sensitive data, network boundaries, or deployment configuration at all
- Only explaining security concepts, not reviewing or modifying a specific implementation
- The current output is purely visual/copy, and there is no external data or permission boundary
Iron Law
Three-level boundary system
| Level | Rule | Example |
|---|---|---|
| Always Do | Validate input before submission, parameterized queries, hashed passwords, HTTPS only cookies | Input sanitization, bcrypt, helmet |
| Ask First | Leaking user data, changing the permission model, introducing authentication dependencies, encryption algorithm selection | New OAuth provider, encryption library selection |
| Never Do | Committing secrets, trusting client-side validation,eval() user input, innerHTML with user data, exposing stack traces to users |
.env in the repository,dangerouslySetInnerHTML |
OWASP Top 10 defenses
1. Injection (SQL/NoSQL/Command)
// Bad: SQL 拼接
const task = await db.query(`SELECT * FROM tasks WHERE id = '${req.params.id}'`);
// Good: 参数化查询
const task = await db.tasks.findById(req.params.id);
// 命令行参数必须通过 exec/spawn 的参数数组,不拼接字符串
2. Cross-site scripting (XSS)
// Bad: 直接插入用户内容
<div>{user.bio}</div> // 如果 bio 包含 <script>...
// Good: 框架自动转义 + DOMPurify for rich text
<div>{user.bio}</div> // React 默认转义
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(richBio) }} />
3. Sensitive data exposure
// Bad: 错误暴露内部信息
catch (err) {
res.status(500).json({ error: err.message, stack: err.stack });
}
// Good: 用户可见错误不含内部信息
catch (err) {
logger.error('Task creation failed', { taskId, error: err.message });
res.status(500).json({ error: { code: 'INTERNAL_ERROR', message: '处理请求时发生错误' } });
}
4. Broken access control
// Bad: 任何登录用户都能删任何任务
app.delete('/tasks/:id', auth, async (req, res) => {
await db.tasks.delete(req.params.id);
});
// Good: 验证所有权
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. Security misconfiguration
// 必须: helmet、CORS 白名单、HTTPS 重定向、安全 cookies
app.use(helmet());
app.use(cors({ origin: ALLOWED_ORIGINS }));
app.use(session({ cookie: { secure: true, httpOnly: true, sameSite: 'strict' } }));
6. Vulnerable dependencies
npm audit # 检查已知 CVE
npm audit fix # 自动修复兼容补丁
Classification:
- Critical/High → must fix (fix or replace the dependency)
- Moderate → assess actual exploitability (may only be exploitable under specific conditions)
- Low → schedule but do not block release
Input validation
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(),
});
Rule: All external input is validated at the boundary (API handler). After validation, trust internally.
Secrets management
项目结构:
├── .env.example # 模板(可提交)
├── .env # 真实密钥(在 .gitignore,绝不提交)
└── src/config.ts # 从环境变量读取
.gitignore 必须包含:
.env
.env.local
*.pem
*.key
credentials.json
Check whether accidentally committed:
git log --all --full-history -- '*.env' '*.pem' 'credentials.*'
Good and bad examples
Good — OWASP checklist verified item by item
A01 Injection: all SQL parameterized ✓ → A02 XSS: framework auto-escaping + DOMPurify ✓ → A04 Access control: every write endpoint verifies ownership ✓ → A06 Dependencies: npm audit has no high/critical ✓ → Input validation: all external input validated with Zod ✓. Each item has a pass/fail determination.
Bad — "we have no user input" so no check is done
"This is just an internal tool, user input was validated on the frontend" — the backend has no input validation, a single line of curl can bypass the frontend. SQL concatenation, non-parameterized queries, no permission checks. After the intranet is compromised, lateral movement is unimpeded.
Output template
Security review conclusion (merged into the Security axis block of 04-review.md ):
## 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 问题: <列出> (如有)
- 建议: <列出>
Verification evidence
The output or record must include:
- Input/source: the spec, plan, code, feedback, or release context that was read.
- Actions performed: The checks, generation, fixes, exports, or release steps actually completed.
- Verification results: Commands, review conclusions, artifact paths, screenshots, or manual confirmation.
- Blockers/rollback: Failed items, rollback paths, or issues requiring a human partner's decision.
Common excuses
| Excuse | Reality | Consequence |
|---|---|---|
| "It's only used internally, not exposed externally" | The intranet is not a trust boundary. Dependency confusion, internal lateral movement—internal applications still need security hardening. | After the intranet is breached, lateral movement faces no obstacles; one compromised intranet machine = the entire intranet compromised. The Target data breach infiltrated through an air-conditioning supplier's intranet. |
| "Everything npm audit reports is low risk" | Low risks can combine into high risk. At least read the audit report once. | Attack chains exploiting combinations of low-risk CVEs have occurred many times (Log4j was a "low-risk" logging library); ignoring audit = gambling. |
| "User input has already been validated on the frontend" | Frontend validation is UX optimization, not security. An attacker can send curl and bypass it directly. | One line of curl -X POST can bypass all frontend validation. Input not validated on the backend = a direct entry point for SQL injection / XSS. |
| "This is a simple internal tool" | A simple tool exposed with one unvalidated input can be used as an intranet pivot. | After an internal tool is breached through phishing or a supply chain attack, it becomes an intranet pivot, expanding the scope of damage from 1 tool to the entire infrastructure. |
| "We'll strengthen security later" | Security debt is different from technical debt—security debt can lead to data breaches. It cannot be patched later. | Data breach compensation + reputational damage + user churn. The cost of remediation after a security incident is 10-50x the cost of prevention (IBM security report). |
Violating the letter of the rules is violating the spirit. There is no gray area.
Verification failure handling
| Failure scenario | Handling method |
|---|---|
| SQL/command injection discovered (OWASP A01) | Blocking — immediately send back to build for fixing; after fixing, resubmit for security review; cannot go live |
| XSS / innerHTML user data discovered (OWASP A02) | Blocking — immediately send back to build for fixing; must use parameterization or DOMPurify |
| Secret or token hardcoded/mistakenly committed | Blocking — immediately remove the secret, rotate the leaked secret value, add to .gitignore, resubmit |
| Missing access control (OWASP A04) | Blocking — add ownership verification or permission checks, re-review after fixing |
| npm audit has Critical/High CVE | Blocking — can only go live after fixing or replacing the dependency; Low can be scheduled but not ignored |
| An unvalidated input point exists | Blocking — all external input must be validated at the boundary; an omission = a potential injection entry point |
Red flag — STOP
- User input reaches SQL/Shell/HTML without parameterization/escaping
- A secret or token appears in code or logs
Authorization: Bearer <token>over a connection without HTTPS- Using
<or>String comparison for permission checks (bypassable) - Authentication endpoint without rate limiting (brute force)
innerHTML、dangerouslySetInnerHTML、eval()Contains user data- Stack traces returned by the API contain file paths or internal library names
Verification checklist
- All user input validated at the boundary (Zod / Yup / Joi)
- SQL queries use parameterization (no string concatenation)
- User content in HTML is escaped
- Error responses do not contain stack traces or internal paths
- Resource ownership is verified for every write endpoint
- Secrets are not in the code repository
-
npm audit(or equivalent) no critical/high - Security headers set (helmet, CORS allowlist, secure cookies)