会话管理的核心就一句话:登录成功后必须换一个新的会话 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 秒实验
- 把
pop改成只生成新 ID 而不删旧的,看第一轮的攻击还能不能成。 - 给会话加上 10 秒空闲过期,再跑一遍第二轮。
- 想一想:为什么改密码时应该作废该账号的所有会话?
常见误区
- 「会话固定需要先偷到 ID」——不需要,攻击者一开始就有。
- 「注销只要清 cookie」——服务端记录不清,ID 仍然有效。
- 「登录换 ID 只是优化」——它是防会话固定的必需动作。
自测三问(点开对答案)
1. 会话固定攻击的关键条件是什么? —— 登录成功后会话 ID 保持不变。 2. 防御会话固定的标准做法是什么? —— 登录成功时生成新 ID,并作废旧 ID。 3. 注销时除了清 cookie 还要做什么? —— 删除服务端的会话记录,让那串 ID 立即失效。第八章完了。下一章往上看一层:HTTP 之外,还长出了哪些协议。
- ☐ (本章完,下一节 9.1 基于HTTP的协议)