litefs.middleware.csrf 源代码

#!/usr/bin/env python
# coding: utf-8

"""
CSRF 保护中间件

提供跨站请求伪造防护
"""

import hashlib
import hmac
import json
import secrets
from typing import Optional

from .base import Middleware


[文档] class CSRFMiddleware(Middleware): """ CSRF 保护中间件 提供跨站请求伪造防护,通过生成和验证 CSRF 令牌来保护表单提交 """ def __init__( self, app, secret_key: Optional[str] = None, token_name: str = "csrf_token", header_name: str = "X-CSRFToken", cookie_name: str = "csrftoken", cookie_secure: bool = False, cookie_http_only: bool = True, cookie_same_site: str = "Lax", exempt_methods: list = None, ): """ 初始化 CSRF 中间件 Args: app: Litefs 应用实例 secret_key: 用于生成 CSRF 令牌的密钥 token_name: 表单中 CSRF 令牌的字段名 header_name: HTTP 头中 CSRF 令牌的名称 cookie_name: 存储 CSRF 令牌的 cookie 名称 cookie_secure: 是否使用安全 cookie cookie_http_only: 是否使用 HTTP only cookie cookie_same_site: SameSite 策略 exempt_methods: 不需要 CSRF 保护的 HTTP 方法 """ super(CSRFMiddleware, self).__init__(app) self.secret_key = secret_key or secrets.token_urlsafe(32) self.token_name = token_name self.header_name = header_name self.cookie_name = cookie_name self.cookie_secure = cookie_secure self.cookie_http_only = cookie_http_only self.cookie_same_site = cookie_same_site self.exempt_methods = exempt_methods or ["GET", "HEAD", "OPTIONS", "TRACE"]
[文档] def process_request(self, request_handler): """ 处理请求,验证 CSRF 令牌 Args: request_handler: 请求处理器实例 Returns: 如果 CSRF 验证失败,返回 403 响应 否则返回 None,继续处理请求 """ # 对于不需要 CSRF 保护的方法,直接通过 request_method = request_handler.environ.get("REQUEST_METHOD", "GET") if request_method in self.exempt_methods: return None # 对于 API 路由,跳过 CSRF 保护 path = request_handler.environ.get("PATH_INFO", "/") if path.startswith("/api/"): return None # 获取 CSRF 令牌 csrf_token = self._get_csrf_token(request_handler) if not csrf_token: return self._create_forbidden_response("CSRF token missing") # 验证 CSRF 令牌 if not self._validate_csrf_token(csrf_token, request_handler): return self._create_forbidden_response("CSRF token invalid") return None
[文档] def process_response(self, request_handler, response): """ 处理响应,设置 CSRF 令牌 cookie Args: request_handler: 请求处理器实例 response: 响应数据 Returns: 添加了 CSRF 令牌 cookie 的响应数据 """ # 生成 CSRF 令牌 csrf_token = self._generate_csrf_token(request_handler) # 设置 CSRF 令牌 cookie if isinstance(response, tuple) and len(response) == 3: status, headers, content = response headers = list(headers) else: # 使用 request_handler 的状态码,而不是硬编码 200 from litefs.handlers import http_status_codes status_code = getattr(request_handler, '_status_code', 200) status_text = http_status_codes.get(status_code, "OK") status = f"{status_code} {status_text}" headers = [] content = response # 添加 CSRF 令牌 cookie cookie_header = self._create_csrf_cookie(csrf_token) headers.append(("Set-Cookie", cookie_header)) # 将 CSRF 令牌添加到响应中,方便前端使用 request_handler._csrf_token = csrf_token return status, headers, content
def _generate_csrf_token(self, request_handler): """ 生成 CSRF 令牌(HMAC-SHA256 签名方式) 令牌格式: <随机nonce>.<HMAC签名> 签名内容: nonce + session_id,绑定到当前会话 Args: request_handler: 请求处理器实例 Returns: CSRF 令牌 """ # 从 cookie 中获取现有令牌,验证其是否仍有效 cookie = request_handler.cookie if cookie: cookie_item = cookie.get(self.cookie_name) if cookie_item: existing_token = cookie_item.value if self._validate_csrf_token(existing_token, request_handler): return existing_token # 生成新的 HMAC-SHA256 签名令牌 nonce = secrets.token_urlsafe(32) session_id = self._get_session_id(request_handler) signature = self._sign_token(nonce, session_id) csrf_token = f"{nonce}.{signature}" return csrf_token def _sign_token(self, nonce: str, session_id: str) -> str: """ 使用 HMAC-SHA256 对 nonce 和 session_id 进行签名 Args: nonce: 随机字符串 session_id: 会话 ID Returns: 签名的十六进制字符串 """ message = f"{nonce}:{session_id}".encode("utf-8") key = self.secret_key.encode("utf-8") return hmac.new(key, message, hashlib.sha256).hexdigest() def _get_session_id(self, request_handler) -> str: """ 获取当前请求的 session_id,用于绑定 CSRF 令牌到会话 Args: request_handler: 请求处理器实例 Returns: session_id 字符串,如果没有会话则返回空字符串 """ session_id = getattr(request_handler, 'session_id', None) if session_id: return str(session_id) # 尝试从 session 属性获取 session = getattr(request_handler, 'session', None) if session and hasattr(session, 'id'): return str(session.id) return "" def _get_csrf_token(self, request_handler): """ 获取 CSRF 令牌 Args: request_handler: 请求处理器实例 Returns: CSRF 令牌 """ # 从 HTTP 头中获取 header_token = request_handler.environ.get( f'HTTP_{self.header_name.upper().replace("-", "_")}', "" ) if header_token: return header_token # 从表单中获取 if hasattr(request_handler, "post"): form_token = request_handler.post.get(self.token_name, "") if form_token: return form_token # 从 cookie 中获取 cookie = request_handler.cookie if cookie: cookie_item = cookie.get(self.cookie_name) if cookie_item: cookie_token = cookie_item.value if cookie_token: return cookie_token return None def _validate_csrf_token(self, token, request_handler): """ 验证 CSRF 令牌(HMAC-SHA256 签名验证) 验证流程: 1. 检查令牌格式(必须包含 nonce.signature) 2. 使用 secret_key 和当前 session_id 重新计算签名 3. 使用恒定时间比较防止时序攻击 Args: token: 要验证的 CSRF 令牌 request_handler: 请求处理器实例 Returns: 如果令牌有效返回 True,否则返回 False """ if not token or len(token) < 64: return False # 解析令牌格式: nonce.signature parts = token.rsplit(".", 1) if len(parts) != 2: return False nonce, signature = parts if not nonce or not signature: return False # 使用当前 session_id 重新计算签名 session_id = self._get_session_id(request_handler) expected_signature = self._sign_token(nonce, session_id) # 使用恒定时间比较,防止时序攻击 return hmac.compare_digest(signature, expected_signature) def _create_csrf_cookie(self, csrf_token): """ 创建 CSRF 令牌 cookie Args: csrf_token: CSRF 令牌 Returns: cookie 字符串 """ from http.cookies import SimpleCookie cookie = SimpleCookie() cookie[self.cookie_name] = csrf_token cookie[self.cookie_name]['path'] = "/" if self.cookie_secure: cookie[self.cookie_name]['secure'] = True if self.cookie_http_only: cookie[self.cookie_name]['httponly'] = True if self.cookie_same_site: cookie[self.cookie_name]['samesite'] = self.cookie_same_site return str(cookie[self.cookie_name]) def _create_forbidden_response(self, message): """ 创建 403 响应 Args: message: 错误消息 Returns: 403 响应 """ status = "403 Forbidden" headers = [ ("Content-Type", "application/json; charset=utf-8"), ] content = json.dumps({"error": message}).encode("utf-8") return status, headers, content