flawopen.com/Vulnerabilities/会话固定与 Cookie 安全
会话固定:登录时轮换 ID(CWE-384)
High Severity
CWE-384
Cryptography & Auth
💡 通俗解释:被粘住的行李领取凭条
想象一名旅客被诱导使用攻击者已经知道的行李领取号码。如果航空公司在托运行李后仍保留这个号码,攻击者就可能冒领行李。会话固定也有类似的边界错误:应用接受攻击者能够让受害者使用的会话标识,并在登录后不更换标识,直接把它升级为已认证会话。两个条件缺一不可;除非应用接受或允许设置该标识,否则普通钓鱼链接本身并不能固定会话。
目标: 服务端会话管理,以及已认证浏览器使用的会话标识。
前提条件: 攻击者能够让受害者使用其已知的会话 ID,例如应用接受 URL 中的会话 ID,或存在 Cookie 注入问题。
缺陷: 应用在匿名会话转为已认证会话时保留了攻击者已知的 ID。
修复: 认证时生成不可预测的新 ID;针对不同威胁配置 Cookie 属性。
核心概念与子系统术语
- 会话固定
- 会话管理缺陷:应用接受攻击者已知的标识,并在受害者登录后继续保留该标识。
- 会话 ID 轮换
- 当认证导致会话权限提升时,签发新的、不可预测的标识。只保留应用确实需要继续使用的会话数据。
- Cookie 属性
Secure将 Cookie 传输限制为 HTTPS;HttpOnly阻止 JavaScript 读取 Cookie;SameSite控制部分跨站 Cookie 发送。它们应对不同风险,不能替代 ID 轮换。__Host-前缀- 在支持该机制的浏览器中,要求 Cookie 通过 HTTPS 设置并带有 Secure、
Path=/,且不含Domain属性。它有助于阻止相邻子域为父域设置同名 Cookie。
分步骤攻击流程
Step 1
攻击者获取或选择会话标识
另一个漏洞或功能使攻击者能够获取 ID,并让受害者的浏览器发送该 ID。具体方式取决于应用;如果应用不接受 URL 中的会话 ID,普通钓鱼链接并不足以完成这一步。
Step 2
受害者使用攻击者已知的标识
浏览器将该 ID 发给应用,应用把它当作有效的匿名会话。攻击者必须能够识别或再次使用同一个标识。
Step 3
认证改变会话权限
受害者登录。如果应用把已认证身份绑定到原有 ID 而不轮换它,这个攻击者已知的标识就代表已认证会话。
Step 4
攻击者重用未变化的标识
只要攻击者仍能发送该 ID 且服务器尚未使其失效,攻击者就可能访问受害者的会话。在权限改变时轮换 ID 可以切断这条攻击链。
机制与根本原因
会话固定同时要求攻击者影响受害者使用的标识,并且应用在登录成功时没有替换它。Cookie 属性可以纵深防御,但都不能替代 ID 轮换:HttpOnly 限制脚本读取,Secure 限制为 HTTPS 传输,SameSite 减少部分跨站发送,__Host- 则在支持的浏览器中限制 Cookie 作用域。
login_view.py(不安全模式)易受攻击
# VULNERABLE: custom session code keeps the pre-authentication ID.
def login_view(request):
user = authenticate(request, request.POST["username"],
request.POST["password"])
if user is not None:
# BUG: privilege changes, but the session ID does not.
request.session["user_id"] = user.pk
request.session["authenticated"] = True
return redirect("/account/")
return redirect("/login/")
login_view.py(Django 安全示例)加固防护
# HARDENED: Django login() rotates the session key on authentication.
from django.contrib.auth import authenticate, login
def login_view(request):
user = authenticate(request, request.POST["username"],
request.POST["password"])
if user is not None:
# Uses Django's supported authentication/session integration.
login(request, user)
return redirect("/account/")
return redirect("/login/")
# settings.py: use HTTPS, host-only scope, and a deliberate SameSite policy.
SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_PATH = "/"
SESSION_COOKIE_DOMAIN = None
攻击序列与概念示例
- 1前提条件: 另一个漏洞或功能允许攻击者影响受害者浏览器将发送的 ID。
- 2已知 ID: 攻击者能够识别或重用同一个匿名会话 ID。
- 3身份认证: 受害者登录时,浏览器仍继续发送攻击者已知的 ID。
- 4未轮换: 应用提升现有会话的权限,却没有替换其标识。
- 5有条件的影响: 只有当已知 ID 仍有效且可用时,攻击者才能访问已认证会话。
工程防护与预防规则
- ✓权限变化时轮换 ID: 在登录及其他权限提升时生成不可预测的新 ID;按照框架文档使旧 ID 失效。
- ✓使用框架认证 API: 优先使用框架支持的登录和会话集成(Django 使用
django.contrib.auth.login()),不要手动把身份字段写入会话。 - ✓拒绝攻击者可控的 ID: 不接受 URL 中的会话 ID。防止相邻子域注入 Cookie,并测试自定义 Cookie/会话处理逻辑。
- ✓按实际用途设置 Cookie 属性: 使用
Secure和HttpOnly;考虑使用带Path=/且没有Domain的__Host-名称。根据合法的跨站登录流程选择SameSite=Lax或Strict;需要 CSRF token 时,SameSite 不能替代它。 - ✓验证会话转换: 在自动化测试中记录匿名 ID、完成认证、断言 ID 已变化,并确认旧 ID 无法访问已认证会话。
来源与相关安全主题:完整安全目录 →