security-and-hardening
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSecurity and Hardening
安全与加固
Overview
概述
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
Web应用的安全优先开发实践:将所有外部输入视为恶意内容,所有密钥视为核心敏感信息,所有权限校验视为强制要求。安全不是某个独立阶段——它是每一行涉及用户数据、身份验证或外部系统的代码都需要遵守的约束。
When to Use
适用场景
- Building anything that accepts user input
- Implementing authentication or authorization
- Storing or transmitting sensitive data
- Integrating with external APIs or services
- Adding file uploads, webhooks, or callbacks
- Handling payment or PII data
- 构建任何接收用户输入的功能
- 实现身份验证或授权逻辑
- 存储或传输敏感数据
- 与外部API或服务集成
- 新增文件上传、webhook或回调功能
- 处理支付或个人身份信息(PII)
The Three-Tier Boundary System
三层边界系统
Always Do (No Exceptions)
必须始终执行(无例外)
- Validate all external input at the system boundary (API routes, form handlers)
- Parameterize all database queries — never concatenate user input into SQL
- Encode output to prevent XSS (use framework auto-escaping, don't bypass it)
- Use HTTPS for all external communication
- Hash passwords with bcrypt/scrypt/argon2 (never store plaintext)
- Set security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- Use httpOnly, secure, sameSite cookies for sessions
- Run (or equivalent) before every release
npm audit
- 在系统边界(API路由、表单处理函数)验证所有外部输入
- 所有数据库查询使用参数化查询——永远不要将用户输入拼接进SQL语句
- 对输出进行编码以防范XSS(使用框架自带的自动转义能力,不要绕过该机制)
- 所有外部通信使用HTTPS
- 使用bcrypt/scrypt/argon2哈希存储密码(永远不要存储明文密码)
- 配置安全头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
- 会话Cookie使用httpOnly、secure、sameSite属性
- 每次发布前执行(或对应语言/工具的等价检查命令)
npm audit
Ask First (Requires Human Approval)
需先申请(需要人工审批)
- Adding new authentication flows or changing auth logic
- Storing new categories of sensitive data (PII, payment info)
- Adding new external service integrations
- Changing CORS configuration
- Adding file upload handlers
- Modifying rate limiting or throttling
- Granting elevated permissions or roles
- 新增身份验证流程或修改鉴权逻辑
- 存储新类别的敏感数据(PII、支付信息)
- 新增外部服务集成
- 修改CORS配置
- 新增文件上传处理逻辑
- 修改限流或流量削峰规则
- 授予高权限角色或权限
Never Do
绝对禁止
- Never commit secrets to version control (API keys, passwords, tokens)
- Never log sensitive data (passwords, tokens, full credit card numbers)
- Never trust client-side validation as a security boundary
- Never disable security headers for convenience
- Never use or
eval()with user-provided datainnerHTML - Never store sessions in client-accessible storage (localStorage for auth tokens)
- Never expose stack traces or internal error details to users
- 永远不要向版本控制提交密钥(API密钥、密码、令牌)
- 永远不要记录敏感数据(密码、令牌、完整信用卡号)
- 永远不要将客户端校验作为安全边界
- 永远不要为了便利禁用安全头
- 永远不要对用户提供的数据使用或
eval()innerHTML - 永远不要将会话存储在客户端可访问的存储中(比如用localStorage存储鉴权令牌)
- 永远不要向用户暴露栈追踪或内部错误详情
OWASP Top 10 Prevention
OWASP Top 10 防护
1. Injection (SQL, NoSQL, OS Command)
1. 注入漏洞(SQL、NoSQL、操作系统命令)
typescript
// 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 } });typescript
// BAD: 字符串拼接导致SQL注入
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// GOOD: 参数化查询
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD: 自带参数化输入能力的ORM
const user = await prisma.user.findUnique({ where: { id: userId } });2. Broken Authentication
2. 身份验证失效
typescript
// 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, // From environment, not code
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));typescript
// 密码哈希
import { hash, compare } from 'bcrypt';
const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);
// 会话管理
app.use(session({
secret: process.env.SESSION_SECRET, // 来自环境变量,不硬编码在代码中
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // 禁止JavaScript访问
secure: true, // 仅在HTTPS下传输
sameSite: 'lax', // CSRF防护
maxAge: 24 * 60 * 60 * 1000, // 24小时有效期
},
}));3. Cross-Site Scripting (XSS)
3. 跨站脚本(XSS)
typescript
// 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);typescript
// BAD: 将用户输入直接渲染为HTML
element.innerHTML = userInput;
// GOOD: 使用框架自动转义(React默认开启该能力)
return <div>{userInput}</div>;
// 如果必须渲染HTML,先做消毒处理
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);4. Broken Access Control
4. 访问控制失效
typescript
// 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 to modify this task' }
});
}
// Proceed with update
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});typescript
// 始终校验权限,而不仅仅是校验是否登录
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
const task = await taskService.findById(req.params.id);
// 校验当前登录用户是否拥有该资源的所有权
if (task.ownerId !== req.user.id) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: '无权限修改该任务' }
});
}
// 继续执行更新逻辑
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});5. Security Misconfiguration
5. 安全配置错误
typescript
// Security headers (use helmet for Express)
import helmet from 'helmet';
app.use(helmet());
// Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
},
}));
// CORS — restrict to known origins
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));typescript
// 安全头配置(Express应用可使用helmet)
import helmet from 'helmet';
app.use(helmet());
// 内容安全策略配置
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // 可收紧的话请进一步限制
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
},
}));
// CORS —— 仅允许已知来源
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));6. Sensitive Data Exposure
6. 敏感数据暴露
typescript
// Never return sensitive fields in API responses
function sanitizeUser(user: UserRecord): PublicUser {
const { passwordHash, resetToken, ...publicFields } = user;
return publicFields;
}
// Use environment variables for secrets
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');typescript
// 永远不要在API响应中返回敏感字段
function sanitizeUser(user: UserRecord): PublicUser {
const { passwordHash, resetToken, ...publicFields } = user;
return publicFields;
}
// 使用环境变量存储密钥
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY未配置');Input Validation Patterns
输入校验模式
Schema Validation at Boundaries
边界处的Schema校验
typescript
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(),
});
// Validate at the route handler
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(),
},
});
}
// result.data is now typed and validated
const task = await taskService.create(result.data);
return res.status(201).json(task);
});typescript
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: '输入不合法',
details: result.error.flatten(),
},
});
}
// result.data 已经完成类型校验和格式校验
const task = await taskService.create(result.data);
return res.status(201).json(task);
});File Upload Safety
文件上传安全
typescript
// Restrict file types and sizes
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
function validateUpload(file: UploadedFile) {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new ValidationError('File type not allowed');
}
if (file.size > MAX_SIZE) {
throw new ValidationError('File too large (max 5MB)');
}
// Don't trust the file extension — check magic bytes if critical
}typescript
// 限制文件类型和大小
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
function validateUpload(file: UploadedFile) {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new ValidationError('不允许的文件类型');
}
if (file.size > MAX_SIZE) {
throw new ValidationError('文件过大(最大支持5MB)');
}
// 不要信任文件扩展名——如果是高敏感场景请校验文件魔术字节
}Triaging npm audit Results
npm audit 结果分级处理
Not all audit findings require immediate action. Use this decision tree:
npm audit reports a vulnerability
├── Severity: critical or high
│ ├── Is the vulnerable code reachable in your app?
│ │ ├── YES --> Fix immediately (update, patch, or replace the dependency)
│ │ └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker
│ └── Is a fix available?
│ ├── YES --> Update to the patched version
│ └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
├── Severity: moderate
│ ├── Reachable in production? --> Fix in the next release cycle
│ └── Dev-only? --> Fix when convenient, track in backlog
└── Severity: low
└── Track and fix during regular dependency updatesKey questions:
- Is the vulnerable function actually called in your code path?
- Is the dependency a runtime dependency or dev-only?
- Is the vulnerability exploitable given your deployment context (e.g., a server-side vulnerability in a client-only app)?
When you defer a fix, document the reason and set a review date.
不是所有的审计发现都需要立即修复,可参考如下决策树:
npm audit 报告漏洞
├── 严重级别:critical 或 high
│ ├── 漏洞代码在你的应用中是否可达?
│ │ ├── 是 --> 立即修复(更新、打补丁或替换依赖)
│ │ └── 否(仅开发依赖、代码路径未使用) --> 尽快修复,但不阻塞发布
│ └── 是否有可用的修复方案?
│ ├── 是 --> 更新到已修复的版本
│ └── 否 --> 查找替代方案,考虑替换依赖,或加入白名单并设置复查日期
├── 严重级别:moderate
│ ├── 生产环境可达? --> 在下个发布周期修复
│ └── 仅开发依赖? --> 方便的时候修复,加入待办跟踪
└── 严重级别:low
└── 跟踪并在常规依赖更新时修复核心判断问题:
- 漏洞函数在你的代码路径中是否真的会被调用?
- 该依赖是运行时依赖还是仅开发依赖?
- 结合你的部署上下文,该漏洞是否可被利用?(比如客户端应用中的服务端漏洞是无法被利用的)
如果你选择推迟修复,请记录原因并设置复查日期。
Rate Limiting
限流
typescript
import rateLimit from 'express-rate-limit';
// General API rate limit
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 10 attempts per 15 minutes
}));typescript
import rateLimit from 'express-rate-limit';
// 通用API限流规则
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个窗口最多100次请求
standardHeaders: true,
legacyHeaders: false,
}));
// 鉴权接口使用更严格的限流规则
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 15分钟内最多10次尝试
}));Secrets Management
密钥管理
.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
*.keyAlways check before committing:
bash
undefined.env 文件规范:
├── .env.example → 提交到版本控制(带占位符的模板文件)
├── .env → 不提交(存储真实密钥)
└── .env.local → 不提交(本地环境覆盖配置)
.gitignore 必须包含:
.env
.env.local
.env.*.local
*.pem
*.key提交前必须检查:
bash
undefinedCheck for accidentally staged secrets
检查是否有不小心暂存的密钥
git diff --cached | grep -i "password|secret|api_key|token"
undefinedgit diff --cached | grep -i "password|secret|api_key|token"
undefinedSecurity Review Checklist
安全审查清单
markdown
undefinedmarkdown
undefinedAuthentication
身份验证
- Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
- Session tokens are httpOnly, secure, sameSite
- Login has rate limiting
- Password reset tokens expire
- 密码使用bcrypt/scrypt/argon2哈希存储(加盐轮数≥12)
- 会话令牌配置了httpOnly、secure、sameSite属性
- 登录接口配置了限流
- 密码重置令牌有过期时间
Authorization
授权
- Every endpoint checks user permissions
- Users can only access their own resources
- Admin actions require admin role verification
- 每个接口都校验用户权限
- 用户仅能访问自己拥有的资源
- 管理员操作需要额外校验管理员角色
Input
输入
- All user input validated at the boundary
- SQL queries are parameterized
- HTML output is encoded/escaped
- 所有用户输入都在系统边界完成校验
- SQL查询都使用参数化查询
- HTML输出做了编码/转义
Data
数据
- No secrets in code or version control
- Sensitive fields excluded from API responses
- PII encrypted at rest (if applicable)
- 代码和版本控制中没有硬编码密钥
- API响应中排除了敏感字段
- 个人身份信息(如需要)在存储时加密
Infrastructure
基础设施
- Security headers configured (CSP, HSTS, etc.)
- CORS restricted to known origins
- Dependencies audited for vulnerabilities
- Error messages don't expose internals
undefined- 配置了安全头(CSP、HSTS等)
- CORS仅允许已知来源
- 完成了依赖漏洞审计
- 错误信息不会暴露内部细节
undefinedCommon Rationalizations
常见错误认知
| Rationalization | Reality |
|---|---|
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
| 错误认知 | 实际情况 |
|---|---|
| "这是内部工具,不需要考虑安全" | 内部工具也会被攻破,攻击者永远会瞄准最薄弱的环节 |
| "我们后面再加安全机制" | 事后补安全的成本是内置安全的10倍,现在就加 |
| "没人会来攻击这个功能" | 自动化扫描器会发现漏洞,靠隐蔽性保障安全不是真正的安全 |
| "框架会处理安全问题" | 框架只提供工具,不提供安全保障,你还是需要正确使用这些工具 |
| "这只是个原型" | 原型最终会变成生产环境,安全习惯要从第一天养成 |
Red Flags
危险信号
- User input passed directly to database queries, shell commands, or HTML rendering
- Secrets in source code or commit history
- API endpoints without authentication or authorization checks
- Missing CORS configuration or wildcard () origins
* - No rate limiting on authentication endpoints
- Stack traces or internal errors exposed to users
- Dependencies with known critical vulnerabilities
- 用户输入直接传入数据库查询、shell命令或HTML渲染逻辑
- 源代码或提交历史中存在密钥
- API接口没有身份验证或授权校验
- 缺少CORS配置或使用通配符()来源
* - 鉴权接口没有限流
- 向用户暴露栈追踪或内部错误
- 依赖存在已知的严重漏洞
Verification
验证
After implementing security-relevant code:
- shows no critical or high vulnerabilities
npm audit - No secrets in source code or git history
- All user input validated at system boundaries
- Authentication and authorization checked on every protected endpoint
- Security headers present in response (check with browser DevTools)
- Error responses don't expose internal details
- Rate limiting active on auth endpoints
实现安全相关代码后,检查以下项:
- 没有critical或high级别的漏洞
npm audit - 源代码或git历史中没有密钥
- 所有用户输入都在系统边界完成校验
- 所有受保护接口都做了身份验证和授权校验
- 响应中包含安全头(可通过浏览器DevTools检查)
- 错误响应不会暴露内部细节
- 鉴权接口已开启限流