Web会话管理的攻与防
HTTP是无状态协议,会话管理通过Cookie、Token等机制维持用户状态。会话安全的核心是确保会话标识符不被窃取或伪造。
| 机制 | 存储位置 | 安全依赖 | 典型漏洞 |
|---|---|---|---|
| Cookie-Session | 服务端 | Cookie属性 | 劫持、CSRF、固定 |
| JWT | 客户端 | 签名算法 | 算法混淆、密钥泄露 |
| OAuth Token | 客户端/服务端 | 授权码流程 | 重定向劫持、PKCE缺失 |
# 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)
# 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
# 攻击页面:
# 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属性
Set-Cookie: session=abc; SameSite=Strict; Secure; HttpOnly
# Chrome 80+ 默认 SameSite=Lax
# 大部分CSRF攻击已被浏览器自动防御
# 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)
});
# 无状态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 = 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配置
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唯一标识 + 黑名单