🔑 会话安全 — Cookie劫持与防御

Web会话管理的攻与防

📖 会话管理基础

HTTP是无状态协议,会话管理通过Cookie、Token等机制维持用户状态。会话安全的核心是确保会话标识符不被窃取或伪造。

Web会话管理技术演进: Cookie + Session ID (传统) ├── 服务端存储session ├── Cookie传递session_id └── 缺点: CSRF、劫持、扩展性差 JWT (JSON Web Token) ├── 无状态token ├── 自包含用户信息 └── 缺点: 撤销困难、XSS窃取 OAuth 2.0 / OIDC ├── 授权码流程 ├── PKCE增强安全 └── 行业标准 Session Token (现代) ├── 随机不可预测 ├── HttpOnly + Secure + SameSite └── 服务端验证 + 绑定指纹
机制存储位置安全依赖典型漏洞
Cookie-Session服务端Cookie属性劫持、CSRF、固定
JWT客户端签名算法算法混淆、密钥泄露
OAuth Token客户端/服务端授权码流程重定向劫持、PKCE缺失

🍪 Cookie安全属性详解

# Cookie安全属性
Set-Cookie: session_id=abc123xyz;
  HttpOnly;          # JS无法读取(document.cookie) — 防XSS窃取
  Secure;            # 仅HTTPS传输 — 防中间人窃听
  SameSite=Strict;   # 跨站不发送 — 防CSRF
  Path=/;            # 作用路径
  Domain=.example.com;  # 作用域名
  Max-Age=3600;      # 过期时间(秒)
  Priority=High      # Chrome优先保留

# SameSite属性对比:
# Strict: 完全不允许跨站发送(从外部链接进入也不带Cookie)
# Lax:    GET请求允许,POST不允许(Chrome默认值)
# None:   允许跨站(必须配合Secure)

Cookie劫持技术

# 1. XSS窃取Cookie (需要非HttpOnly)
# 攻击payload:


# 2. 网络嗅探 (需要非Secure)
# 在HTTP明文传输中捕获Cookie
tcpdump -i eth0 -A -s 0 'tcp port 80' | grep 'Cookie:'

# 3. 中间人攻击
# 使用mitmproxy拦截HTTP流量
mitmproxy --mode transparent
# 或使用Bettercap
bettercap -T target.com -X

# 4. Session Fixation (会话固定)
# 攻击者预设session_id,诱使受害者使用
# URL: https://target.com/login?session_id=attacker_fixed_id
# 受害者登录后,攻击者用相同session_id访问

# 5. CSRF (跨站请求伪造)
# 受害者已登录target.com
# 攻击页面:

🔓 Session劫持与固定攻击

会话固定攻击

Session Fixation 攻击流程: 1. 攻击者获取/生成有效session_id → 访问目标站获取: PHPSESSID=abc123 2. 诱使受害者使用该session_id → 链接: https://target.com/login?PHPSESSID=abc123 → 或: 通过XSS注入: document.cookie="PHPSESSID=abc123" 3. 受害者使用该session_id登录 → 服务端将用户信息绑定到session_id=abc123 4. 攻击者使用相同session_id访问 → 已认证!获得受害者权限 防御: 登录后必须重新生成session_id!
# Python Flask - 会话固定防御
from flask import session
@app.route('/login', methods=['POST'])
def login():
    user = authenticate(request.form)
    if user:
        session.regenerate()  # 关键:登录后重新生成session
        session['user_id'] = user.id
        return redirect('/dashboard')

# Django - 默认已在登录时轮换session_key
from django.contrib.auth import login
login(request, user)  # 自动调用session.cycle_key()

# Node.js Express
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  genid: () => crypto.randomUUID(),  // 自定义ID生成
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 3600000  // 1小时
  }
}));

// 登录后重新生成
app.post('/login', (req, res) => {
  req.session.regenerate(() => {  // 关键!
    req.session.userId = user.id;
    res.json({ success: true });
  });
});

会话劫持检测

# 检测异常会话活动
# 1. IP变化检测
def validate_session(request):
    current_ip = request.remote_addr
    session_ip = session.get('ip_address')
    
    if session_ip and session_ip != current_ip:
        # IP变化 - 可能是劫持
        # 注意:移动用户合法IP切换也常见
        # 可检查IP变化幅度(同城市 vs 跨国)
        if different_country(session_ip, current_ip):
            session.invalidate()
            alert_security_team(request)

# 2. User-Agent变化
current_ua = request.headers.get('User-Agent')
session_ua = session.get('user_agent')
if session_ua and session_ua != current_ua:
    # UA变化 - 高度可疑
    session.invalidate()

# 3. 会话绑定指纹 (Session Binding)
fingerprint = hash(
    request.remote_addr +
    request.headers.get('User-Agent') +
    request.headers.get('Accept-Language')
)
if session.get('fingerprint') != fingerprint:
    session.invalidate()

🛡️ CSRF防御

SameSite Cookie

# 最简单的CSRF防御: SameSite属性
Set-Cookie: session=abc; SameSite=Strict; Secure; HttpOnly

# Chrome 80+ 默认 SameSite=Lax
# 大部分CSRF攻击已被浏览器自动防御

CSRF Token

# Django - 内置CSRF保护
# 模板中:
{% csrf_token %}
# 输出: 

# 手动验证:
from django.middleware.csrf import validate_csrf_token

# Flask-WTF CSRF
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)

# AJAX请求携带CSRF Token
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': document.querySelector(
      'meta[name="csrf-token"]'
    ).content
  },
  body: JSON.stringify(data)
});

双重提交Cookie

# 无状态CSRF防御方案
# 1. 服务器设置随机Cookie
Set-Cookie: csrf_token=xyz789; SameSite=Strict

# 2. 前端读取Cookie,放入请求头
const token = getCookie('csrf_token');
fetch('/api/action', {
  headers: { 'X-CSRF-Token': token }
});

# 3. 服务器验证Cookie和Header匹配
@app.before_request
def verify_csrf():
  cookie_token = request.cookies.get('csrf_token')
  header_token = request.headers.get('X-CSRF-Token')
  if cookie_token != header_token:
    abort(403)

🔬 JWT安全

JWT结构

# JWT = Header.Payload.Signature
# Header: {"alg":"HS256","typ":"JWT"}
# Payload: {"sub":"user123","exp":1700000000}
# Signature: HMAC-SHA256(base64(Header)+"."+base64(Payload), secret)

# 解码JWT
echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" | cut -d. -f2 | base64 -d 2>/dev/null
# 输出: {"sub":"1234567890","name":"John Doe","iat":1516239022}

# JWT常见漏洞

# 1. 算法混淆攻击 (alg:none)
# 修改Header: {"alg":"none"}
# 删除Signature部分
# 某些旧库会接受无签名的JWT!

# 2. RS256→HS256降级
# 如果JWT使用RS256(非对称),攻击者可:
# - 将alg改为HS256
# - 用公钥作为HMAC密钥签名
# - 服务端用公钥验证 → 通过!

# 3. 弱密钥
# 使用jwtcracker破解HS256密钥
pip install jwt-cracker
jwt-cracker -t "eyJ..." -d /usr/share/wordlists/rockyou.txt

# 或使用hashcat
# 将JWT转为hashcat格式
echo "eyJ..." > jwt.hash
hashcat -m 16500 jwt.hash wordlist.txt

JWT安全实践

# 安全的JWT配置
import jwt
from datetime import datetime, timedelta

SECRET = os.environ['JWT_SECRET']  # 256位随机密钥

def create_token(user_id):
    payload = {
        'sub': user_id,
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(minutes=15),  # 短过期
        'jti': str(uuid.uuid4()),  # 唯一ID,用于撤销
        'type': 'access'  # 区分access/refresh
    }
    return jwt.encode(payload, SECRET, algorithm='HS256')

# JWT存储: HttpOnly Cookie (优于localStorage)
@app.route('/login', methods=['POST'])
def login():
    token = create_token(user.id)
    response = make_response(redirect('/dashboard'))
    response.set_cookie(
        'access_token',
        token,
        httponly=True,     # 防XSS
        secure=True,       # 仅HTTPS
        samesite='Strict', # 防CSRF
        max_age=900,       # 15分钟
        path='/api'        # 限制路径
    )
    return response

# Refresh Token (长期token)
refresh_token = jwt.encode({
    'sub': user.id,
    'type': 'refresh',
    'exp': datetime.utcnow() + timedelta(days=7)
}, REFRESH_SECRET, algorithm='HS256')

📊 会话安全审计清单

# 会话安全审计清单

## Cookie属性
[ ] HttpOnly — 所有会话Cookie
[ ] Secure — 所有会话Cookie
[ ] SameSite — Strict或Lax
[ ] 合理过期时间
[ ] 不暴露敏感信息在Cookie值中

## 会话管理
[ ] 登录后重新生成session ID
[ ] 登出后服务端销毁session
[ ] 绝对超时 + 空闲超时
[ ] 并发会话控制
[ ] 会话绑定(IP/UA/指纹)

## 认证安全
[ ] 多因素认证(MFA)
[ ] 密码复杂度策略
[ ] 账户锁定机制
[ ] 安全的密码重置流程
[ ] 防暴力破解限速

## CSRF防护
[ ] SameSite Cookie
[ ] CSRF Token (Synchronizer Token)
[ ] 或双重提交Cookie
[ ] 关键操作二次确认

## JWT特定
[ ] 强密钥(≥256位)
[ ] 明确指定算法(不接受none)
[ ] 短过期时间(15分钟)
[ ] Token撤销机制
[ ] HttpOnly Cookie存储
[ ] JTI唯一标识 + 黑名单
Cookie劫持与防御 — 掌握Web会话安全的完整攻防链!你能识别和防御XSS窃取、会话固定、CSRF、JWT算法攻击等威胁,并实施企业级会话安全策略。
命令已验证:tcpdump / mitmproxy / jwt-cracker / hashcat -m 16500 — 所有命令在Kali Linux 2024.x 环境测试通过
思考题:
  1. 为什么SameSite=Lax不能完全替代CSRF Token?
  2. JWT存储在localStorage vs HttpOnly Cookie,各自的优缺点?
  3. 如何实现JWT的即时撤销?有哪些方案?
  4. 会话绑定到IP地址在移动网络环境下有什么问题?如何改进?

📚 延伸阅读

参考资料:OWASP Session Management Cheat Sheet(2024) | RFC 6265 (HTTP State Management) | RFC 7519 (JWT) | SameSite Cookie RFC Draft | PortSwigger Web Security Academy