Python后端接口如果不做请求体安全校验,等于把数据库大门直接敞开。很多人以为加了JWT或OAuth就万事大吉,实际上恶意数据绕过认证后直接打到业务逻辑层的情况非常普遍。请求体校验的核心不是“有没有做”,而是“在哪个层面做、校验哪些字段、失败后如何响应”。真正有效的做法是把校验前置到视图函数入口,配合Pydantic或Marshmallow做结构化约束,同时对字符串长度、数值范围、枚举值、嵌套结构、未知字段进行严格限制。
请求体安全校验的第一原则:不信任任何输入无论前端做了多少表单验证,后端必须重新校验。前端验证只是用户体验层,攻击者可以直接用curl、Postman或自写脚本构造原始HTTP请求。请求体中的JSON、XML、表单数据、文件上传字段都可能被篡改。常见风险包括:超长字符串导致数据库截断异常或内存消耗、负数金额、越界分页参数、类型混淆(字符串传数字字段)、嵌套对象深度过大导致递归解析崩溃、注入特殊字符触发SQL或命令拼接。安全校验不是简单判断字段是否存在,而是对每个字段的类型、长度、格式、范围、业务合法性做完整约束。
用Pydantic做请求体模型校验FastAPI生态中Pydantic是事实标准。定义一个请求体模型,声明每个字段的类型和约束条件,框架会在进入业务逻辑前自动解析并校验。示例:
from pydantic import BaseModel, Field, EmailStr, validator
from typing import Optional, List
from datetime import datetime
class OrderItem(BaseModel):
product_id: int = Field(..., ge=1, description="商品ID必须为正整数")
quantity: int = Field(..., ge=1, le=999, description="单件商品数量限制1-999")
unit_price: float = Field(..., ge=0.01, le=999999.99, description="单价范围限制")
class CreateOrderRequest(BaseModel):
user_id: int = Field(..., ge=1)
email: EmailStr
items: List[OrderItem] = Field(..., min_items=1, max_items=50)
coupon_code: Optional[str] = Field(None, max_length=32)
remark: Optional[str] = Field(None, max_length=500)
created_at: datetime = Field(default_factory=datetime.utcnow)
@validator('coupon_code')
def validate_coupon(cls, v):
if v and not v.isalnum():
raise ValueError('优惠码只能包含字母和数字')
return v.upper()
这段代码直接限制了商品ID必须大于等于1,数量在1到999之间,单价有上下限,订单商品列表至少1项最多50项,邮箱必须符合格式,优惠码只能字母数字并自动转大写。任何不符合规则的数据都会在进入视图函数前被拦截,返回422状态码和结构化错误信息。这样攻击者无法通过构造超大数量、负数金额、非法邮箱来探测系统漏洞。
未知字段拦截与额外属性控制Pydantic默认会忽略请求体中多余的字段,这在安全上存在隐患。攻击者可以发送大量无关字段,试图探测后端模型结构或触发反序列化漏洞。建议在模型配置中禁止额外字段:
class StrictBaseModel(BaseModel):
class Config:
extra = 'forbid'
max_anystr_length = 1000
max_nesting_depth = 5
所有请求体模型继承这个基类,任何未声明的字段都会导致422错误。max_anystr_length限制任意字符串最大长度,防止超长文本攻击;max_nesting_depth限制嵌套深度,防止构造深层JSON导致解析器栈溢出。这两个配置在Pydantic v2中非常实用,很多团队忽略了它们。
针对JSON请求体的原始层防护即使用了Pydantic,仍需在框架层面对原始请求体大小做限制。Starlette/FastAPI默认不限制请求体大小,攻击者可以发送几百MB的JSON让服务器内存耗尽。在ASGI层面或反向代理层设置请求体上限:
# 使用中间件限制请求体大小
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
class LimitRequestBodyMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
content_length = request.headers.get('content-length')
if content_length and int(content_length) > 5 * 1024 * 1024: # 5MB
return JSONResponse(status_code=413, content={"detail": "请求体过大"})
return await call_next(request)
这个中间件在请求进入路由前检查Content-Length头,超过5MB直接返回413。注意Content-Length可以被伪造或省略,所以还需要在读取请求体时做流式限制。对于Flask或Django,同样需要在WSGI层或网关层设置body大小限制,不能只依赖应用层模型校验。
字符串字段的深度清洗与编码安全长度限制只是第一步。字符串内容可能包含控制字符、Unicode混淆字符、零宽字符、HTML标签、SQL片段。对于不同用途的字段,清洗策略不同:
import re
import html
def sanitize_text(value: str, max_len: int = 200) -> str:
# 移除控制字符(保留换行和制表符)
value = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', value)
# 移除零宽字符
value = re.sub(r'[\u200b-\u200f\u202a-\u202e\ufeff]', '', value)
# 统一换行符
value = value.replace('\r\n', '\n').replace('\r', '\n')
# 截断超长
if len(value) > max_len:
value = value[:max_len]
return value.strip()
对于会回显到前端的字段,还需要做HTML转义防止存储型XSS。对于会拼接到SQL的字段,必须使用参数化查询而不是手动拼接。对于会写入日志的字段,要防止日志注入(换行符伪造日志记录)。这些清洗逻辑应该放在Pydantic的validator中,确保进入业务层的数据已经干净。
数值字段的范围与类型严格校验JSON中的数字在Python里可能被解析为int或float,但攻击者可以发送超大整数(超过数据库INT范围)、NaN、Infinity、科学计数法表示的数字。Pydantic默认会拒绝NaN和Infinity,但需要显式设置范围。对于金额字段,建议使用Decimal而不是float,避免浮点精度问题:
from decimal import Decimal
from pydantic import condecimal
class PaymentRequest(BaseModel):
amount: condecimal(ge=Decimal('0.01'), le=Decimal('1000000'), max_digits=10, decimal_places=2)
user_id: conint(ge=1, le=999999999)
condecimal和conint是Pydantic提供的约束类型,直接限制小数位数和整数范围。如果业务中金额计算涉及汇率或分成,Decimal的精度控制能避免很多安全与财务问题。对于分页参数,限制page在1到10000之间,page_size在1到100之间,防止攻击者请求超大分页拖垮数据库。
枚举字段与白名单校验状态字段、类型字段、排序字段必须使用枚举或白名单,不能接受任意字符串。攻击者经常通过枚举字段注入非法值,探测后端分支逻辑或触发未处理异常。使用Python的Enum配合Pydantic:
from enum import Enum
class OrderStatus(str, Enum):
PENDING = 'pending'
PAID = 'paid'
SHIPPED = 'shipped'
CANCELLED = 'cancelled'
class UpdateOrderRequest(BaseModel):
status: OrderStatus
sort_by: Literal['created_at', 'amount', 'status']
Literal类型在Pydantic中可以直接限制字段只能取特定值。任何不在白名单中的值都会被拒绝,避免攻击者传入“admin”“deleted”“all”等特殊值触发越权或批量操作。
嵌套结构深度与列表长度限制请求体中的嵌套JSON对象和数组是攻击者重点利用的地方。深层嵌套会导致递归解析性能下降,超长列表会导致数据库批量插入压力。除了Pydantic的max_nesting_depth,还需要对列表字段设置min_items和max_items。对于树形结构数据(如评论、分类、组织架构),需要额外限制树的深度和节点总数:
def validate_tree_depth(node, current_depth=0, max_depth=5):
if current_depth > max_depth:
raise ValueError('树结构深度超过限制')
children = node.get('children', [])
if len(children) > 20:
raise ValueError('单层子节点数量超过限制')
for child in children:
validate_tree_depth(child, current_depth + 1, max_depth)
这种递归校验放在业务逻辑入口处,防止攻击者构造深度为1000的嵌套JSON导致栈溢出或数据库递归查询超时。
文件上传字段的特殊校验如果接口涉及文件上传,请求体可能是multipart/form-data格式。此时需要校验文件大小、MIME类型、文件扩展名、文件内容魔数。不能只信任客户端声明的Content-Type,必须读取文件头判断真实类型:
import magic
def validate_upload_file(file_bytes: bytes, max_size: int = 10 * 1024 * 1024):
if len(file_bytes) > max_size:
raise ValueError('文件大小超过10MB限制')
mime = magic.from_buffer(file_bytes[:1024], mime=True)
allowed_mimes = {'image/jpeg', 'image/png', 'application/pdf'}
if mime not in allowed_mimes:
raise ValueError(f'不允许的文件类型: {mime}')
# 检查文件头魔数
if mime == 'image/jpeg' and not file_bytes.startswith(b'\xff\xd8\xff'):
raise ValueError('伪造的JPEG文件')
if mime == 'image/png' and not file_bytes.startswith(b'\x89PNG\r\n\x1a\n'):
raise ValueError('伪造的PNG文件')
文件上传是高风险区域,除了类型和大小,还需要对文件名做清洗(移除路径穿越字符如../),存储时使用随机生成的文件名而不是用户提供的原始文件名。
失败响应的信息泄露控制校验失败时返回的错误信息不能泄露内部结构。Pydantic默认的422响应会包含字段名和错误类型,这本身是合理的,但不要在生产环境返回详细的堆栈信息或数据库错误。自定义异常处理器,统一返回格式:
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
errors = []
for error in exc.errors():
errors.append({
'field': '.'.join(str(loc) for loc in error['loc'][1:]),
'message': error['msg']
})
return JSONResponse(status_code=422, content={'code': 422, 'message': '请求参数校验失败', 'errors': errors})
这样返回的错误信息结构化且不暴露内部代码路径。对于生产环境,还可以选择隐藏具体的字段错误,只返回通用提示,避免攻击者通过错误信息探测接口结构。
结合限流与审计日志形成纵深防御请求体校验只是第一道防线。即使校验逻辑完善,攻击者仍可能通过大量合法格式的请求进行业务攻击(如刷单、撞库、枚举用户ID)。需要在接口层面叠加限流策略,对单个IP、用户、设备指纹做频率控制。同时记录关键操作的审计日志,包括请求体摘要(脱敏后)、校验失败原因、来源IP、时间戳。日志中不能记录密码、令牌、身份证号等敏感字段的明文。
Python后端接口的请求体安全校验是一个系统工程,需要从模型层、框架层、中间件层、业务层多个维度协同。Pydantic提供了强大的声明式校验能力,但开发者必须主动设置约束条件,而不是使用宽松的默认配置。对未知字段的禁止、字符串深度的清洗、数值范围的严格限制、枚举白名单的强制、嵌套结构的深度控制、文件上传的内容检测,每一项都直接关系到接口能否抵御恶意数据。安全校验的最终目标不是“让正常请求通过”,而是“让异常请求在造成破坏前被精准拦截”。
