Session固定漏洞的原理分析与实战防御方案
先看效果,再看代码
上周上线一个内部管理系统,第二天就收到安全组邮件:「检测到 Session 固定漏洞,风险等级中」。我盯着邮件看了三秒,心里一沉——又来了。
不是没防过。但这次是旧项目接入新登录模块,后端用的是 Laravel 9,前端是 Vue 3 + Pinia,Session ID 居然在登录前后完全没变。用户输完账号密码,点登录,服务端直接复用了登录前的 Session ID。攻击者只要提前拿到这个 ID(比如诱导用户访问带 PHPSESSID=abc123 的链接),登录成功后就能直接接管会话。
亲测有效的方式只有一个:**登录成功后必须销毁旧 Session 并生成全新 ID**。别信什么“框架默认处理”,Laravel 默认不 regenerate,Express 默认也不 regenerate,Spring Boot 默认更不 regenerate——都得你自己动手。
核心代码就这几行
先说 Laravel。我在 LoginController.php 的 authenticated() 方法里加了这三行:
public function authenticated(Request $request, $user)
{
// 关键:强制销毁当前 session 并生成新 ID
$request->session()->regenerate(true);
// 后续逻辑...
return redirect()->intended();
}
注意那个 true 参数,它代表「删除旧 Session 文件」。如果不传,只是换 ID,旧文件还在磁盘上,照样能被复用。我踩过坑:第一次只写了 $request->session()->regenerate(),扫描工具照报漏洞。
再来看 Express + Passport 的场景。我们有个 Node.js 管理后台,登录路由是这样写的:
router.post('/login', (req, res, next) => {
passport.authenticate('local', { session: true }, (err, user, info) => {
if (err || !user) {
return res.status(401).json({ error: '登录失败' });
}
// 关键:销毁旧 session,重设新 session
req.session.regenerate((err) => {
if (err) {
return next(err);
}
req.session.userId = user.id;
req.session.authTime = Date.now();
res.json({ success: true });
});
})(req, res, next);
});
这里特别注意:req.session.destroy() 不够,它只清数据不换 ID;req.session.reload() 也不行,ID 没变。必须用 regenerate() ——而且要等它回调完成后再写入新 session 数据。我折腾了半天发现,如果 reload 和写入混在一起,偶尔还是出现固定 ID,最后锁定就是顺序问题。
Vue 前端也要配合?真要
你以为后端改完就完了?错。我们在 Vue 3 里用了 useAuthStore() 管理登录态,登录成功后直接调用 store.setToken(res.data.token),但 token 是 JWT,而 Session ID 是存在 Cookie 里的 connect.sid(Express)或 laravel_session(Laravel)。这时候如果前端没清缓存、没重置状态,旧 session 还可能被意外携带。
所以我们在 login action 里加了强制清理:
// stores/auth.js
const login = async (credentials) => {
try {
const res = await api.post('/login', credentials);
// 关键:通知浏览器丢掉所有旧会话相关缓存
if ('clearSiteData' in window) {
window.clearSiteData?.(['cookies']);
}
// 重置 store 状态(避免残留)
$reset();
// 再初始化新态
userId = res.data.user.id;
authTime = Date.now();
} catch (e) {
throw e;
}
};
window.clearSiteData 是个好东西,Chrome 87+ 支持,虽然 Safari 还不支持,但至少保证主流环境 cookie 被清干净。比手动删 Cookie 可靠得多——手写 document.cookie = 'xxx=; expires=Thu, 01 Jan 1970 00:00:00 GMT' 容易漏 path/domain,还容易写错格式。
踩坑提醒:这三点一定注意
- Cookie 的 HttpOnly 和 SameSite 不能替代 Session 固定防护:很多人以为开了
SameSite=Strict就万事大吉,其实它只防 CSRF,不防 Session 固定。攻击者只要让用户先访问一次带固定 ID 的链接(比如发个邮件内嵌<a href="https://jztheme.com/login?PHPSESSID=deadbeef" rel="external nofollow" >),照样能命中。必须配合 regenerate。 - 不要在登录前预创建 Session:我们有个老接口,用户打开登录页就顺手调了个
/api/user/profile(未鉴权),结果后端一进中间件就自动 new session,导致 ID 提前暴露。后来改成:登录页加载时只请求静态资源,所有 API 都等登录按钮点击后才发起。 - 测试时别用 Postman 或 curl 直接带 Cookie 测:Postman 默认不处理 Set-Cookie 的 domain/path,你看到新 Cookie,实际浏览器根本不会存。最好用 Chrome 开无痕窗口,F12 → Application → Cookies,手动对比登录前后
laravel_session值是否真的变了。我有次扫出来没修复,就是因为 Postman 显示新 Cookie,但浏览器里还是老 ID。
这个场景最好用:SSO 登录跳转
我们有个 SSO 中心,主站 A 登录后跳回子站 B,B 接收 token 后自己建 session。这种链路最容易出问题——A 给的跳转 URL 里带上了自己的 session ID,B 一接收就直接复用。
解决方案很简单:B 在收到跳转请求时,第一件事不是校验 token,而是先 session_destroy()(PHP)或 req.session.destroy()(Node),再 session_start() 或 req.session.regenerate(),最后才走后续流程。哪怕多一次 round-trip,也比被 hijack 强。
以上是我踩坑后的总结,希望对你有帮助
Session 固定不是那种“加个中间件就完事”的问题,它藏在登录流程的毛细血管里:页面加载、API 请求顺序、跳转参数、缓存策略……哪个环节松动一点,都可能让 ID 泄露出去。
目前我们线上项目已全量覆盖 regenerate + clearSiteData + 严格 Cookie 策略,OWASP ZAP 扫描通过率从 62% 拉到 100%。当然,还有些边缘情况没覆盖(比如某些老旧 IE 兼容模式下 clearSiteData 不生效),但至少主线稳了。
这个技巧的拓展用法还有很多,比如和 OAuth2 PKCE 结合做双因子 session 绑定、在微服务网关层统一拦截固定 ID 请求、甚至用 WebAssembly 做客户端 session ID 校验……后续会继续分享这类博客。
以上是我个人对 Session 固定的完整讲解,有更优的实现方式欢迎评论区交流。

暂无评论