name: security-checklist description: Web应用程序部署前的安全审计。在代码审查、现有应用程序审计或用户提到"安全审查"、“准备部署”、"上线生产"或关注漏洞时使用。覆盖认证、输入验证、秘密管理、数据库安全和合规基础。
安全清单
在交付任何Web应用程序前的最低可行安全措施。这不是详尽清单——它是防止明显灾难的基线。
部署前审计
在任何生产部署前运行此清单。如果无法勾选某项,请先修复。
认证
- [ ] 使用bcrypt、scrypt或Argon2哈希密码(绝不用MD5/SHA1/明文)
- [ ] 强制执行密码要求(推荐至少12个字符)
- [ ] 会话令牌是加密随机的(使用
crypto.randomBytes或等效方法) - [ ] 会话过期(普通应用24小时,敏感数据更短)
- [ ] 注销时服务器端实际使会话无效
- [ ] 密码重置令牌是单次使用并在1小时内过期
- [ ] 失败的登录尝试被限速(5次尝试,然后冷却)
- [ ] 代码、日志或错误消息中无凭证
输入验证
- [ ] 所有用户输入在服务器端验证(客户端验证是用户体验,不是安全)
- [ ] SQL查询使用参数化语句(绝不用字符串拼接)
- [ ] HTML输出被转义以防止XSS(使用框架默认设置)
- [ ] 文件上传验证类型、大小,并存储在Web根目录外
- [ ] URL和重定向验证基于允许列表
- [ ] JSON/XML解析器有实体扩展限制
秘密管理
- [ ] 源代码或git历史中无秘密
- [ ] 所有凭证使用环境变量或秘密管理器
- [ ] 开发/测试/生产环境使用不同的秘密
- [ ] API密钥具有最低所需权限
- [ ] 秘密可以在不部署代码的情况下轮换
数据库安全
- [ ] 数据库不暴露给公共互联网
- [ ] 应用程序使用专用数据库用户(非root/admin)
- [ ] 数据库用户具有最低所需权限
- [ ] 启用行级安全(RLS)如适用
- [ ] 备份加密并测试恢复
- [ ] 连接字符串使用SSL/TLS
网络和传输
- [ ] 仅使用HTTPS(重定向HTTP到HTTPS)
- [ ] TLS 1.2+(禁用旧版本)
- [ ] 启用HSTS头部
- [ ] 设置安全的Cookie标志(Secure、HttpOnly、SameSite)
- [ ] CORS为特定来源配置(生产环境中不用
*)
限速和DDoS
- [ ] API端点按用户/IP限速
- [ ] 登录端点有更严格的限制
- [ ] 应用程序前有CDN或DDoS保护
- [ ] 配置请求大小限制
- [ ] 所有外部调用设置超时限制
日志记录和监控
- [ ] 记录认证事件(登录、注销、失败尝试)
- [ ] 日志中无敏感数据(密码、令牌、个人身份信息)
- [ ] 日志安全存储并有保留策略
- [ ] 配置可疑模式警报
- [ ] 错误消息不泄露系统信息
基础设施
- [ ] 配置防火墙(默认拒绝)
- [ ] 关闭不必要的端口
- [ ] SSH密钥认证(无密码认证)
- [ ] 更新依赖(检查已知漏洞)
- [ ] 管理界面不公开访问
按框架的常见漏洞
Node.js/Express
// 错误:SQL注入
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// 正确:参数化查询
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
// 错误:XSS漏洞
res.send(`<h1>Hello ${req.query.name}</h1>`);
// 正确:使用自动转义的模板引擎
res.render('greeting', { name: req.query.name });
// 错误:代码中的秘密
const API_KEY = 'sk_live_abc123';
// 正确:环境变量
const API_KEY = process.env.API_KEY;
Python/Django/Flask
# 错误:SQL注入
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# 正确:参数化查询
cursor.execute("SELECT * FROM users WHERE id = %s", [user_id])
// 错误:Pickle反序列化(RCE漏洞)
data = pickle.loads(user_input)
// 正确:对不受信任数据使用JSON
data = json.loads(user_input)
// 错误:生产环境中的调试模式
app.run(debug=True)
// 正确:关闭调试,适当错误处理
app.run(debug=False)
React/前端
// 错误:通过dangerouslySetInnerHTML的XSS
<div dangerouslySetInnerHTML={{ __html: userContent }} />
// 正确:让React转义内容
<div>{userContent}</div>
// 错误:将令牌存储在localStorage(XSS可访问)
localStorage.setItem('token', authToken);
// 更好:HttpOnly Cookie(无法通过JS访问)
// 通过服务器响应头部设置
// 错误:在前端代码中暴露API密钥
const API_KEY = 'sk_live_abc123';
// 正确:通过后端代理
const response = await fetch('/api/proxy/external-service');
密码哈希参考
使用bcrypt的Node.js
const bcrypt = require('bcrypt');
// 哈希(注册时)
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
// 验证(登录时)
const isValid = await bcrypt.compare(plainPassword, hashedPassword);
使用bcrypt的Python
import bcrypt
# 哈希
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
# 验证
is_valid = bcrypt.checkpw(password.encode('utf-8'), hashed)
环境变量设置
.env文件(仅开发,永不提交)
# .env
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
JWT_SECRET=your-256-bit-secret-here
API_KEY=sk_test_xxxxx
.gitignore(必需)
.env
.env.local
.env.*.local
*.pem
*.key
credentials.json
secrets/
加载环境变量
// 使用dotenv的Node.js
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
// 快速失败如果缺失
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET环境变量必需');
}
数据库行级安全(Supabase/PostgreSQL)
-- 在表上启用RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- 用户只能读取自己的文档
CREATE POLICY "用户可以读取自己的文档" ON documents
FOR SELECT USING (auth.uid() = user_id);
-- 用户只能以自己的身份插入文档
CREATE POLICY "用户可以插入自己的文档" ON documents
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- 用户只能更新自己的文档
CREATE POLICY "用户可以更新自己的文档" ON documents
FOR UPDATE USING (auth.uid() = user_id);
-- 用户只能删除自己的文档
CREATE POLICY "用户可以删除自己的文档" ON documents
FOR DELETE USING (auth.uid() = user_id);
CORS配置
Express.js
const cors = require('cors');
// 错误:允许所有来源
app.use(cors());
// 正确:特定来源
app.use(cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
安全头部
使用Helmet的Express.js
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
手动头部
// 基本安全头部
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
res.setHeader('Content-Security-Policy', "default-src 'self'");
不应记录的内容
// 绝不记录以下内容:
// - 密码(明文或哈希)
// - 会话令牌 / JWTs
// - API密钥
// - 信用卡号
// - 社会安全号
// - 包含用户数据的完整请求体
// 错误
console.log('登录尝试:', { email, password });
console.log('请求:', req.body);
// 正确
console.log('登录尝试:', { email, success: false, reason: 'invalid_password' });
console.log('请求:', { endpoint: req.path, method: req.method, userId: req.user?.id });
常见问题的快速修复
“我将密码存储在明文”
- 立即添加密码哈希
- 强制所有用户重置密码
- 使所有现有会话无效
- 检查数据库是否曾被暴露/泄露
“我的API密钥在git历史中”
- 立即轮换密钥(生成新密钥)
- 撤销旧密钥
- 使用
git filter-branch或BFG Repo-Cleaner从历史中移除 - 强制推送(与团队协调)
“我不知道我在记录什么数据”
- 在代码库中搜索
console.log、logger.、print( - 审查记录的内容
- 实现带有字段允许列表的结构化日志记录
- 设置日志轮转和保留
“我的数据库公开可访问”
- 立即更改数据库凭证
- 配置防火墙规则(仅允许应用服务器IP)
- 为连接启用SSL/TLS
- 审查访问日志以查找未授权查询
合规快速参考
您可能需要关注:
- GDPR(欧盟用户):数据删除权、同意、违规通知
- CCPA(加州用户):类似GDPR
- PCI DSS(信用卡):不存储卡号,使用支付处理器
- HIPAA(健康数据):加密、访问控制、审计日志
- SOC 2(企业销售):安全控制文档
任何用户数据的最低要求:
- 隐私政策说明数据收集
- 用户请求数据删除的方式
- 传输中(HTTPS)和静态加密
- 访问日志和审计追踪
- 事件响应计划(即使是基本的)
资源
- OWASP Top 10:https://owasp.org/www-project-top-ten/
- OWASP Cheat Sheets:https://cheatsheetseries.owasp.org/
- Have I Been Pwned(检查违规):https://haveibeenpwned.com/
- Mozilla Observatory(测试您的头部):https://observatory.mozilla.org/
- SSL Labs(测试您的TLS):https://www.ssllabs.com/ssltest/