这份清单来自几次真实的渗透测试和线上事故复盘。每一条都能对应到具体动作,不只是原则。
一、身份与认证
- 密码哈希用慢哈希:PBKDF2 / bcrypt / Argon2,绝不用 MD5、SHA1、裸 SHA256
- 每个用户独立随机盐,盐不能是用户名或固定值
- 恒定时间比较哈希,避免时序侧信道
- 会话令牌足够随机(≥ 128 bit),且服务端可主动失效
- Cookie 属性正确
const cookie = [
`sid=${token}`, 'Path=/', 'HttpOnly', 'SameSite=Lax',
isHttps ? 'Secure' : '',
].filter(Boolean).join('; ');
- 登录失败提示统一:「用户名或密码错误」,不要区分「用户不存在」
- 有登录限流,按 IP + 用户名双维度
CREATE TABLE login_attempts (
ip TEXT PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0,
first_at TEXT NOT NULL,
locked_until TEXT
);
- 改密码后强制下线所有会话
DELETE FROM sessions WHERE user_id = ?;
二、输入与越权
- 所有 SQL 用参数绑定,禁止字符串拼接
// ❌
db.prepare(`SELECT * FROM posts WHERE slug = '${slug}'`);
// ✅
db.prepare('SELECT * FROM posts WHERE slug = ?').bind(slug);
- 服务端校验所有字段类型与长度,前端校验只是体验优化
- 路径参数防穿越
if (!key || key.includes('..')) return c.text('非法路径', 400);
- 越权检查:查资源时带上归属条件,不要只查 ID
-- ❌ 任何登录用户都能改别人的文章
UPDATE posts SET title = ? WHERE id = ?;
-- ✅
UPDATE posts SET title = ? WHERE id = ? AND author_id = ?;
- 上传限制 MIME + 扩展名 + 大小,并且把文件放在独立域名或路径下,避免和主站同源执行
三、输出与前端
- 所有输出到 HTML 的用户内容都转义
const escapeHtml = (s) => String(s)
.replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
- 富文本/Markdown 走白名单消毒(见另一篇《服务端渲染 Markdown 的正确姿势》)
- 响应头齐全
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; object-src 'none'
Strict-Transport-Security: max-age=31536000; includeSubDomains
- 外链加
rel="noopener noreferrer" - 不用
eval、new Function、innerHTML拼接用户输入
四、传输与配置
- 强制 HTTPS,HTTP 直接 301 跳转
- 密钥不进代码仓库:
.env写进.gitignore,线上用密钥管理服务
// 检查有没有把密钥写进 vars
"vars": { "PBKDF2_ITERATIONS": "100000" } // ✅ 非敏感
// "vars": { "API_KEY": "sk-xxx" } // ❌ 应该用 secret
- 错误页面不暴露堆栈,生产环境只返回错误 ID,细节写日志
- CORS 不要用
*配合credentials: true
// ❌ 浏览器会直接拒绝,或者配置错误导致任意站点可读接口
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
- 关闭目录列表、调试端点(
/debug、/actuator、/.env)
五、依赖与运维
- 锁文件提交进仓库(
pnpm-lock.yaml/package-lock.json),CI 用--frozen-lockfile - 定期跑依赖漏洞扫描
pnpm audit --prod
npx osv-scanner --lockfile=pnpm-lock.yaml
- 容器以非 root 运行
- 数据库最小权限:应用账号不需要 DROP / GRANT
- 日志里有审计线索:谁在什么时候改了什么
logger.info('post.updated', { userId, postId, ip, ua });
- 有备份且验证过恢复流程。「备份没恢复过 = 没有备份」
一份可以直接复制到 PR 模板的简版
## 安全检查
- [ ] 新增接口做了登录校验与越权检查
- [ ] 新增查询使用参数绑定
- [ ] 用户输入输出做了转义/消毒
- [ ] 未在代码或配置中硬编码密钥
- [ ] 新增依赖已跑 audit
- [ ] 错误路径不泄露内部信息
最后
安全没有「做完」的时刻,但把清单变成流程能挡住 90% 的低级问题:写代码时自查、评审时对照、上线前再扫一遍。
最危险的心态是「这是内部系统,没人会打」——大部分被拖库的项目,最初都这么想。