Development
安全与加固
针对漏洞对代码进行加固。在处理用户输入、认证、数据存储或外部集成时使用。
面向 Web 应用的安全优先开发实践。把每一个外部输入都当作敌意输入来对待。
使用场景
- 构建任何接受用户输入的功能
- 实现认证或授权
- 存储或传输敏感数据
- 与外部 API 或服务集成
- 添加文件上传、Webhook 或回调
- 处理支付或 PII(个人身份信息)数据
先做威胁建模
在加固之前,花五分钟像攻击者一样思考:
- 梳理信任边界。 不可信数据从哪里进入你的系统?
- 明确资产。 什么值得被窃取或破坏?
- 对每个边界运行 STRIDE 分析:
| 威胁 | 提问 | 典型缓解措施 |
|---|---|---|
| Spoofing(仿冒) | 是否有人能冒充某个用户/服务? | 认证、签名验证 |
| Tampering(篡改) | 数据能否在传输中或静态时被篡改? | 完整性校验、参数化查询 |
| Repudiation(抵赖) | 某个操作之后能否被否认? | 对安全事件进行审计日志记录 |
| Information disclosure(信息泄露) | 数据是否会泄露? | 加密、字段允许列表 |
| Denial of service(拒绝服务) | 是否会被压垮? | 限流、输入大小上限 |
| Elevation of privilege(权限提升) | 用户能否获得本不应有的权限? | 授权检查、最小权限原则 |
三层边界系统
始终执行(无例外)
- 在系统边界验证所有外部输入
- 参数化所有数据库查询 —— 绝不将用户输入拼接进 SQL
- 对输出进行编码以防止 XSS
- 对所有外部通信使用 HTTPS
- 使用 bcrypt/scrypt/argon2 对密码进行哈希
- 设置安全响应头(CSP、HSTS、X-Frame-Options)
- 会话使用 httpOnly、secure、sameSite cookie
- 在每次发布前运行
npm audit
先询问(需人工批准)
- 添加新的认证流程
- 存储新类别的敏感数据
- 添加新的外部服务集成
- 修改 CORS 配置
- 添加文件上传处理器
绝不执行
- 绝不将密钥提交到版本控制
- 绝不记录敏感数据(密码、令牌、完整的信用卡号)
- 绝不将客户端校验当作安全边界来信任
- 绝不为图方便而禁用安全响应头
- 绝不对用户提供的数据使用
eval()或innerHTML
OWASP Top 10 防护模式
注入(SQL、NoSQL、操作系统命令)
// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });
失效的认证
// Password hashing
import { hash, compare } from 'bcrypt';
const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);
// Session management
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000,
},
}));
跨站脚本(XSS)
// BAD: Rendering user input as HTML
element.innerHTML = userInput;
// GOOD: Use framework auto-escaping (React does this by default)
return <div>{userInput}</div>;
// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
失效的访问控制
// Always check authorization, not just authentication
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
const task = await taskService.findById(req.params.id);
// Check that the authenticated user owns this resource
if (task.ownerId !== req.user.id) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Not authorized' }
});
}
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});
输入验证模式
在边界处进行模式(Schema)校验
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200).trim(),
description: z.string().max(2000).optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
dueDate: z.string().datetime().optional(),
});
app.post('/api/tasks', async (req, res) => {
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: result.error.flatten(),
},
});
}
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
限流
import rateLimit from 'express-rate-limit';
// General API rate limit
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
}));
密钥管理
.env files:
├── .env.example → Committed (template with placeholder values)
├── .env → NOT committed (contains real secrets)
└── .env.local → NOT committed (local overrides)
.gitignore must include:
.env
.env.local
.env.*.local
*.pem
*.key
提交前始终检查:
git diff --cached | grep -i "password\|secret\|api_key\|token"
安全审查清单
- 密码使用 bcrypt/scrypt/argon2 哈希(salt rounds ≥ 12)
- 会话令牌为 httpOnly、secure、sameSite
- 登录有限流
- 每个端点都检查用户权限
- 用户只能访问自己的资源
- 所有用户输入都在边界处校验
- SQL 查询已参数化
- HTML 输出已编码/转义
- 代码或版本控制中无密钥
- 敏感字段已从 API 响应中排除
- 已配置安全响应头(CSP、HSTS 等)
- CORS 限制为已知来源
- 依赖已审计漏洞
- 错误消息不暴露内部信息
验证
在实现与安全相关的代码后:
-
npm audit未显示严重(critical)或高危(high)漏洞 - 源代码或 git 历史中无密钥
- 所有用户输入都在系统边界处校验
- 每个受保护端点都检查了认证与授权
- 响应中包含安全响应头
- 错误响应不暴露内部细节
- 认证端点已启用限流