📘 第八章 确认访问用户身份的认证
📄 一句话概括

会话管理的核心就一句话:登录成功后必须换一个新的会话 ID。 不换就是会话固定攻击——攻击者先把 ID 给你,你用它登录,那个 ID 就自动变成了他的。

📝 本节和前面几节的关系

上一节发了会话 ID,这一节讲怎么管。两个 Demo 轮次一对照,漏洞和修法的差别只有一行代码。

一、会话固定是怎么成的

第一轮 Demo 完整复现了这个过程: 1. 攻击者自己访问一次 /login,服务端给了他一个未登录的 ID(比如 3015a754b964db97); 2. 他想办法让受害者的浏览器用上这个 ID; 3. 受害者在这个 ID 上完成登录; 4. 攻击者拿原来那个 ID 去访问 /me,回 200——他拿到了受害者的数据。 关键点在最后一句:ID 从没变过。攻击者不需要偷,他一开始就有。 所以这种攻击不依赖任何窃取手段,只依赖「登录不换 ID」这一行疏忽。

二、修法只有一句话

第二轮 Demo 只改了一处:登录成功时用 secrets.token_hex(8) 生成新 ID,并把旧 ID 从会话表里删掉。 结果:受害者登录后 ID 变成 f96ee1897b43fafb,攻击者再带原来那个 73a5db5632c1006f 访问, 回 302 跳回登录页——没进去。 代价几乎为零:多生成一串随机数而已。 这是安全里少见的「成本接近 0、收益很明确」的修补,所以它被写进了所有框架的默认行为。 顺带一条:注销时也要把服务端的会话记录删掉,不能只清 cookie。 只清 cookie 的话,那串 ID 在服务端还有效,谁拿到都能继续用。

三、会话管理还有哪些坑

会话表存在哪:单机内存最简单,多机就得放共享存储(Redis 之类)。 放共享存储时要注意别把整个会话对象都序列化进去,只存 ID 和必要字段。 过期策略:绝对过期(登录后 N 小时必失效)加空闲过期(M 分钟不动就失效)。 只设一个的话,要么用户老被踢,要么长期不登录的会话一直挂着。 并发登录:同一账号多处登录要不要互踢? 不踢的话,被盗号后受害者改密码也踢不掉对方——所以改密码时应当作废所有会话。 这几条都没标准可依,全靠自己定。判断标准就一句:会话表是攻击者的目标,所以它必须能被随时作废。

四、动手 Demo:同一段代码,开不开「换 ID」差别巨大

同一套登录逻辑跑两遍:一遍登录前后共用一个 ID,一遍登录成功时重新生成,再作废旧 ID。 两轮都模拟「攻击者先拿到 ID、受害者用它登录、攻击者复用」的全过程。

if USERS.get(form[u'user']) != form[u'pass']:
    return 200 + u'密码不对'

new = secrets.token_hex(8) if (rotate or not sid) else sid
state[u'sessions'][new] = form[u'user']
if sid and rotate:
    state[u'sessions'].pop(sid, None)      # 旧 ID 立刻作废
return 302 + [(u'Set-Cookie', u'SESSIONID=%s; Path=/; HttpOnly' % new)]

实跑输出:

--------------------------------------------------------------------
会话固定:ID 是攻击者先给的那一个
--------------------------------------------------------------------
  第一轮:登录前后都用同一个 ID
    攻击者先拿到 ID     de7667a258cc72dd
    受害者用它登录,ID 变成 de7667a258cc72dd(没换)
    攻击者再带原 ID 访问 /me → HTTP/1.1 200 OK
    结果                拿到了受害者数据

  第二轮:登录成功时重新生成 ID
    攻击者先拿到 ID     009e12c2e3339cf8
    受害者用它登录,ID 变成 e6ea87bd2a71a7f1(换了新的)
    攻击者再带原 ID 访问 /me → HTTP/1.1 302 Found
    结果                没进去

  第一轮就是会话固定攻击的完整过程:
    1. 攻击者自己访问一次 /login,服务端给了他一个未登录的 ID;
    2. 他想办法让受害者的浏览器用上这个 ID
       (早期有办法通过 URL 里的 sessionid 参数做到,或者跨站写 cookie);
    3. 受害者在这个 ID 上完成了登录;
    4. 攻击者手上那个 ID 自动变成了「已登录」。
  关键点是:**ID 从没变过**。攻击者不需要偷,他一开始就有。

  第二轮的防御就一句话:登录成功时换一个新 ID,并作废旧 ID。
  这样攻击者手上那个始终停留在「未登录」状态。
  代价几乎为零——生成一串随机数而已。

第一轮:攻击者拿 3015a754b964db97,受害者用它登录,ID 没换; 攻击者再带原 ID 访问 /me,回 200,拿到受害者数据。 第二轮:攻击者拿 73a5db5632c1006f,受害者登录后 ID 变成 f96ee1897b43fafb; 攻击者带原 ID 访问,回 302,没进去。 两轮代码只差「生成新 ID 并作废旧 ID」这一处。这就是会话固定的全部修法。 注意第二轮里旧 ID 是被删除的,不只是不再使用——留着的话,它仍然是「未登录」状态, 理论上还能被拿去做下一轮攻击。

30 秒实验

  1. 把 pop 改成只生成新 ID 而不删旧的,看第一轮的攻击还能不能成。
  2. 给会话加上 10 秒空闲过期,再跑一遍第二轮。
  3. 想一想:为什么改密码时应该作废该账号的所有会话?

常见误区

  1. 「会话固定需要先偷到 ID」——不需要,攻击者一开始就有。
  2. 「注销只要清 cookie」——服务端记录不清,ID 仍然有效。
  3. 「登录换 ID 只是优化」——它是防会话固定的必需动作。
自测三问(点开对答案) 1. 会话固定攻击的关键条件是什么? —— 登录成功后会话 ID 保持不变。 2. 防御会话固定的标准做法是什么? —— 登录成功时生成新 ID,并作废旧 ID。 3. 注销时除了清 cookie 还要做什么? —— 删除服务端的会话记录,让那串 ID 立即失效。

第八章完了。下一章往上看一层:HTTP 之外,还长出了哪些协议。

相关笔记