FlawOpen

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

攻击序列与概念示例

工程防护与预防规则

来源与相关安全主题:完整安全目录 →
CWE-384:会话固定 → OWASP:会话管理指南 → OWASP WSTG:测试会话固定 → Django:会话与 cycle_key() → MDN:HTTP Cookie 与前缀 →