这份清单来自几次真实的渗透测试和线上事故复盘。每一条都能对应到具体动作,不只是原则。

一、身份与认证

  • 密码哈希用慢哈希: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, '&amp;').replace(/</g, '&lt;')
  .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  • 富文本/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% 的低级问题:写代码时自查、评审时对照、上线前再扫一遍。

最危险的心态是「这是内部系统,没人会打」——大部分被拖库的项目,最初都这么想。