007 - api-security-patterns 📅 发布时间:2026/9/3 7:00:13 👁 浏览次数: API 安全模式与反模式保护 REST API、webhook 和服务间通信的参考。在007 audit、007 threat-model和 API 代码审查时使用。1. 认证模式API 密钥# 正确API 密钥在请求头中Authorization:ApiKey sk-live-abc123def456# 错误API 密钥在 URL 中会记录在服务器日志、浏览器历史、referrer 头中GET /api/data?api_keysk-live-abc123def456# 最佳实践api_keys:-使用前缀识别密钥sk-live-、sk-test-、pk--哈希存储SHA-256而非明文-定期轮换最长 90 天-限定到特定权限/资源-按密钥进行速率限制-一旦泄露立即撤销-不同环境使用不同密钥开发/预发布/生产OAuth 2.0# 按客户端类型推荐的流程oauth2_flows:server_to_server:client_credentialsweb_app_with_backend:authorization_code PKCEsingle_page_app:authorization_code PKCE无客户端密钥mobile_app:authorization_code PKCENEVER_USE:implicit_grant# 已弃用令牌暴露在 URL 中# 令牌最佳实践tokens:access_token_lifetime:15_minutes# 短寿命refresh_token_lifetime:7_days# 使用时轮换refresh_token_rotation:true# 每次刷新令牌store_tokens:httponly_secure_cookie# 非 localStoragerevocation:implement_revocation_endpointJWT 最佳实践# 正确正确的 JWT 配置jwt_config{algorithm:RS256,# 非对称而非使用弱密钥的 HS256expiration:900,# 最长 15 分钟issuer:auth.example.com,# 始终验证audience:api.example.com,# 始终验证required_claims:[sub,exp,iat,iss,aud],}# 需要检测的错误模式jwt_antipatterns[algorithm: none,# 无签名验证algorithm: HS256,# 使用弱/共享密钥exp: far_future,# 永不过期的令牌no audience check,# 跨服务重复使用令牌secret in code,# 硬编码的签名密钥JWT in URL parameter,# 被记录、缓存、通过 referrer 泄露]# 关键始终验证defvalidate_jwt(token:str)-dict:returnjwt.decode(token,keyPUBLIC_KEY,# 非弱共享密钥algorithms[RS256],# 显式指定不从令牌头读取audienceapi.example.com,issuerauth.example.com,options{require:[exp,iat,sub]},)2. 速率限制策略令牌桶# 最适合允许突发流量同时维持平均速率classTokenBucket: capacity100, refill_rate10/sec 允许 100 个请求的突发然后每秒 10 个持续。 def__init__(self,capacity:int,refill_rate:float):self.capacitycapacity self.tokenscapacity self.refill_raterefill_rate self.last_refilltime.time()defallow_request(self)-bool:self._refill()ifself.tokens1:self.tokens-1returnTruereturnFalse滑动窗口# 最适合平滑的速率限制无突发许可# 在时间窗口中跟踪请求统计最近 N 秒内的请求数# Redis 实现ZADD ZRANGEBYSCORE ZCARD按用户速率限制rate_limits:unauthenticated:requests_per_minute:20requests_per_hour:100authenticated_free:requests_per_minute:60requests_per_hour:1000authenticated_paid:requests_per_minute:300requests_per_hour:10000# 始终包含响应头headers:X-RateLimit-Limit:60X-RateLimit-Remaining:45X-RateLimit-Reset:1620000060# Unix 时间戳Retry-After:30# 在 429 响应中3. 输入验证模式验证frompydanticimportBaseModel,Field,validatorclassCreateUserRequest(BaseModel):name:strField(min_length1,max_length100)email:strField(regexr^[a-zA-Z0-9_.-][a-zA-Z0-9-]\.[a-zA-Z0-9-.]$)age:intField(ge13,le150)role:strField(defaultuser)# 如果用户尝试设置admin则忽略validator(role)defrestrict_role(cls,v):ifvnotin(user,viewer):# 仅允许安全角色returnuserreturnvclassConfig:extraforbid# 拒绝未知字段防止批量赋值类型检查和大小限制validation_rules:string_fields:max_length:10_000# 无无界字符串strip_whitespace:truereject_null_bytes:true# \x00 可能导致问题numeric_fields:define_min_max:true# 始终设置边界reject_nan_infinity:true# 可能破坏数学运算array_fields:max_items:100# 无无界数组validate_each_item:truefile_uploads:max_size:10MBallowed_types:[image/jpeg,image/png,application/pdf]validate_magic_bytes:true# 不要仅信任 Content-Type 头scan_for_malware:truequery_parameters:max_page_size:100default_page_size:20max_query_length:5004. Webhook 安全HMAC 签名验证importhmacimporthashlibimporttimedefverify_webhook(payload:bytes,headers:dict,secret:str)-bool:完整的 webhook 验证签名 时间戳。signatureheaders.get(X-Webhook-Signature)timestampheaders.get(X-Webhook-Timestamp)ifnotsignatureornottimestamp:returnFalse# 1. 防止重放攻击5 分钟窗口ifabs(time.time()-int(timestamp))300:returnFalse# 2. 计算预期签名signed_payloadf{timestamp}.{payload.decode()}expectedhmac.new(secret.encode(),signed_payload.encode(),hashlib.sha256).hexdigest()# 3. 常量时间比较防止时序攻击returnhmac.compare_digest(fsha256{expected},signature)Webhook 最佳实践webhook_security:sending:-使用 HMAC-SHA256 签名每个有效载荷-在签名中包含时间戳-发送唯一事件 ID 以实现幂等性-仅使用 HTTPS-实施指数退避重试-定期轮换签名密钥receiving:-在任何处理之前验证签名-拒绝超过 5 分钟的请求重放保护-实施幂等性存储已处理的事件 ID-快速返回 200异步处理-不要盲目信任有效载荷数据验证模式-对传入 webhook 进行速率限制-记录所有 webhook 事件以供审计5. CORS 配置# 危险允许一切# Access-Control-Allow-Origin: *# Access-Control-Allow-Credentials: true # 对 * 来源无效# 安全显式白名单CORS_CONFIG{allowed_origins:[https://app.example.com,https://admin.example.com,],allowed_methods:[GET,POST,PUT,DELETE],allowed_headers:[Authorization,Content-Type],allow_credentials:True,max_age:3600,# 预检缓存1 小时expose_headers:[X-RateLimit-Remaining],}# 需要检测的反模式cors_antipatterns[Access-Control-Allow-Origin: *,# 过于宽松reflect Origin header as Allow-Origin,# 实际上等于带凭证的 *Access-Control-Allow-Origin: null,# 可被利用Allow-Origin without credentials but with auth,# 不一致]6. 安全头检查清单# 所有 API 响应必需的安全头security_headers:# 防止 MIME 嗅探X-Content-Type-Options:nosniff# 防止点击劫持针对 HTML 响应X-Frame-Options:DENY# XSS 保护旧浏览器X-XSS-Protection:0# 禁用改用 CSP# HTTPS 强制Strict-Transport-Security:max-age31536000; includeSubDomains; preload# 内容安全策略针对 HTML 响应Content-Security-Policy:default-src self; script-src self; style-src self# Referrer 策略Referrer-Policy:strict-origin-when-cross-origin# 权限策略Permissions-Policy:camera(), microphone(), geolocation()# 移除服务器信息头Server:REMOVE_THIS_HEADERX-Powered-By:REMOVE_THIS_HEADER# 敏感数据的缓存控制Cache-Control:no-store, no-cache, must-revalidate, privatePragma:no-cache7. 常见 API 漏洞BOLA / IDOR对象级授权失效# 有漏洞无所有权检查app.get(/api/users/{user_id}/orders)defget_orders(user_id:int):returndb.query(Order).filter(Order.user_iduser_id).all()# 任何已认证用户都可以访问其他用户的订单# 安全强制所有权检查app.get(/api/users/{user_id}/orders)defget_orders(user_id:int,current_user:UserDepends(get_current_user)):ifcurrent_user.id!user_idandnotcurrent_user.is_admin:raiseHTTPException(403,禁止访问)returndb.query(Order).filter(Order.user_iduser_id).all()批量赋值# 有漏洞接受请求中的所有字段app.put(/api/users/{user_id})defupdate_user(user_id:int,data:dict):db.query(User).filter(User.iduser_id).update(data)# 攻击者发送 {role: admin, is_verified: true}# 安全可更新字段的显式白名单classUserUpdateRequest(BaseModel):name:str|NoneNoneemail:str|NoneNone# role 和 is_verified 不包含在内app.put(/api/users/{user_id})defupdate_user(user_id:int,data:UserUpdateRequest):db.query(User).filter(User.iduser_id).update(data.dict(exclude_unsetTrue))数据过度暴露# 有漏洞返回整个数据库模型app.get(/api/users/{user_id})defget_user(user_id:int):returndb.query(User).get(user_id).__dict__# 返回id, name, email, password_hash, ssn, internal_notes, ...# 安全显式响应模式classUserResponse(BaseModel):id:intname:stremail:str# 仅公开字段app.get(/api/users/{user_id},response_modelUserResponse)defget_user(user_id:int):returndb.query(User).get(user_id)8. 幂等性模式# 防止重复处理同一请求# 对以下操作至关重要支付、webhook、任何非幂等操作classIdempotencyMiddleware: 客户端发送Idempotency-Key: unique-uuid-here 服务器存储结果重试时返回缓存响应。 def__init__(self,cache):self.cachecache# Redis 或类似asyncdefprocess(self,idempotency_key:str,handler):# 1. 检查是否已处理cachedawaitself.cache.get(fidempotency:{idempotency_key})ifcached:returncached# 返回与首次相同的响应# 2. 加锁防止并发重复处理lockawaitself.cache.lock(flock:{idempotency_key},timeout30)ifnotlock:raiseHTTPException(409,请求已在处理中)try:# 3. 处理请求resultawaithandler()# 4. 缓存结果24 小时 TTLawaitself.cache.set(fidempotency:{idempotency_key},result,ttl86400,)returnresultfinally:awaitlock.release()何时需要幂等性密钥require_idempotency_key:-POST /payments-POST /transfers-POST /orders-POST /webhooks/*# 使用事件 ID 作为密钥-任何非幂等变更操作naturally_idempotent:# 无需密钥-GET所有-PUT完整替换-DELETE按 ID快速安全审查检查清单认证 [ ] 所有端点需要认证除非明确公开 [ ] API 密钥在请求头中而非 URL 中 [ ] JWT 使用 RS256 和短有效期 [ ] 公开客户端使用 OAuth 2.0 PKCE [ ] 实施了令牌轮换 授权 [ ] 每次数据访问有所有权检查BOLA 预防 [ ] 每个特权操作有角色检查 [ ] 批量赋值保护显式字段白名单 [ ] 响应模式过滤敏感字段 输入/输出 [ ] 所有输入有模式验证 [ ] 所有字段、数组和文件有大小限制 [ ] 参数化查询无字符串拼接 [ ] 通用错误消息无堆栈跟踪 传输 [ ] 所有地方使用 HTTPSTLS 1.2 [ ] 设置了安全头 [ ] CORS 已显式配置 [ ] 已启用 HSTS 运维 [ ] 每用户/IP 的速率限制 [ ] 带有关联 ID 的请求日志 [ ] webhook 签名已验证 [ ] 变更操作的幂等性密钥 [ ] 依赖项已扫描 CVE