← 中文资源

简体中文版本

加固会话 Cookie 防窃取与 CSRF

设置 HttpOnly、Secure、SameSite 和 __Host- 属性,登录时轮换会话 ID,并验证浏览器拒绝不安全的 Cookie 写入。

阅读英文原文

概述

Cookie 属性是一条链,不是勾选清单。HttpOnly 阻止 JavaScript 读取 Cookie,限制 XSS 窃取,但不能阻止注入脚本发起已认证请求;Secure 阻止明文 HTTP 传输;SameSite=Lax 阻止大多数跨站 POST 携带 Cookie;__Host- 前缀要求 Cookie 必须 Secure、Path=/ 且不能设置 Domain。它们各自覆盖不同失败模式。注意 __Host- 是部署约束:本地 HTTP 无法使用同名 Cookie,开发环境应使用 HTTPS 和受信任证书,或者换一个较弱的名称,不能为了本地方便削弱生产配置。

步骤

配置 express-session 和 Redis 存储:

app.use(session({
  name: '__Host-sid',
  secret: process.env.SESSION_SECRET!,
  store: new RedisStore({ client: redis, prefix: 'sess:' }),
  resave: false,
  saveUninitialized: false,
  rolling: true,
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    path: '/',
    maxAge: 8 * 60 * 60 * 1000,
  },
}));

登录时验证 Argon2 密码哈希,然后调用 req.session.regenerate,在回调里写入 userId 并显式 save 后再返回成功。退出时调用 req.session.destroy 删除服务端记录,并用相同 path 清除 Cookie。只清 Cookie 不删 Redis 记录时,被窃取的 Cookie 仍然有效。

再加 Origin 校验作为纵深防御:把它放在登录/退出路由之前,对非 GET/HEAD/OPTIONS 请求,Origin 必须完全等于 PUBLIC_ORIGIN,否则返回 403;PUBLIC_ORIGIN 缺失时启动失败,不能默认放行所有来源。

验证

检查登录响应头:

curl -sS -D - -o /dev/null https://localhost:3000/login \
  --insecure -H 'origin: https://localhost:3000' \
  -H 'content-type: application/json' \
  -d '{"email":"[email protected]","password":"correct horse battery staple"}' \
  | grep -i '^set-cookie:'

应看到 __Host-sid=、Path=/、HttpOnly、Secure、SameSite=Lax,且没有 Domain。再向 /logout 发送带 Origin: https://evil.example 的 POST,必须返回 403。用同一 Cookie jar 连续登录两次,比较 Cookie 值,应得到两个不同会话 ID,证明登录时确实重新生成了会话。浏览器 DevTools 中执行 document.cookie,不应出现 __Host-sid。

注意事项

secure: true 要求端到端 HTTPS;反向代理层数配置错误会让 Express 认为请求不安全,从而不设置 Cookie。__Host- 被浏览器拒绝时,检查是否意外设置了 Domain 或 Path 不是 /。Redis 的会话 TTL 应与 Cookie 的 maxAge 对齐。rolling: true 会延长活跃用户的会话,如需要固定时限,还要加服务端绝对超时。HttpOnly 不能替代 CSP,SameSite=Lax 也不能防御同站子域攻击。