736 lines
22 KiB
Markdown
736 lines
22 KiB
Markdown
# OWASP Top 10 (2021) 防护指南
|
||
|
||
> 本文档为安全专家技能的参考资料,涵盖 OWASP Top 10 每一项的风险描述、攻击示例、防护代码和检查清单。
|
||
> 代码示例基于 FastAPI (Python) 和 Next.js (TypeScript) 技术栈。
|
||
|
||
---
|
||
|
||
## A01: 访问控制失效 (Broken Access Control)
|
||
|
||
### 风险描述
|
||
|
||
访问控制确保用户只能在其权限范围内操作。失效的访问控制允许攻击者越权访问、修改或删除数据。常见问题包括:IDOR(不安全的直接对象引用)、越权访问、路径遍历、CORS 配置错误。
|
||
|
||
### 攻击示例
|
||
|
||
```
|
||
# IDOR 攻击:修改 URL 中的 ID 访问他人数据
|
||
GET /api/users/1001/profile → GET /api/users/1002/profile
|
||
|
||
# 越权操作:普通用户访问管理接口
|
||
POST /api/admin/delete-user
|
||
```
|
||
|
||
### FastAPI 防护代码
|
||
|
||
```python
|
||
from fastapi import Depends, HTTPException, status
|
||
|
||
# 资源级别权限校验 — 防止 IDOR
|
||
async def get_order(order_id: int, current_user: User = Depends(get_current_user)):
|
||
order = await db.get(Order, order_id)
|
||
if not order:
|
||
raise HTTPException(status_code=404, detail="订单不存在")
|
||
# 关键:校验资源归属
|
||
if order.user_id != current_user.id and not current_user.has_role("admin"):
|
||
raise HTTPException(status_code=403, detail="无权访问此资源")
|
||
return order
|
||
|
||
# 基于装饰器的权限守卫
|
||
from functools import wraps
|
||
|
||
def require_roles(*roles: str):
|
||
def decorator(func):
|
||
@wraps(func)
|
||
async def wrapper(*args, current_user: User = Depends(get_current_user), **kwargs):
|
||
if not any(role in current_user.roles for role in roles):
|
||
raise HTTPException(status_code=403, detail="权限不足")
|
||
return await func(*args, current_user=current_user, **kwargs)
|
||
return wrapper
|
||
return decorator
|
||
|
||
@app.delete("/api/admin/users/{user_id}")
|
||
@require_roles("admin", "super_admin")
|
||
async def delete_user(user_id: int, current_user: User = Depends(get_current_user)):
|
||
await db.delete(User, user_id)
|
||
return {"message": "用户已删除"}
|
||
```
|
||
|
||
### Next.js 防护代码
|
||
|
||
```typescript
|
||
// middleware.ts — 路由级别访问控制
|
||
import { NextResponse } from "next/server";
|
||
import type { NextRequest } from "next/server";
|
||
import { verifyToken } from "@/lib/auth";
|
||
|
||
const PROTECTED_ROUTES: Record<string, string[]> = {
|
||
"/admin": ["admin", "super_admin"],
|
||
"/dashboard": ["admin", "user"],
|
||
};
|
||
|
||
export async function middleware(request: NextRequest) {
|
||
const token = request.cookies.get("access_token")?.value;
|
||
if (!token) {
|
||
return NextResponse.redirect(new URL("/login", request.url));
|
||
}
|
||
|
||
const payload = await verifyToken(token);
|
||
if (!payload) {
|
||
return NextResponse.redirect(new URL("/login", request.url));
|
||
}
|
||
|
||
// 检查路由权限
|
||
for (const [path, roles] of Object.entries(PROTECTED_ROUTES)) {
|
||
if (request.nextUrl.pathname.startsWith(path)) {
|
||
if (!roles.includes(payload.role)) {
|
||
return NextResponse.redirect(new URL("/unauthorized", request.url));
|
||
}
|
||
}
|
||
}
|
||
|
||
return NextResponse.next();
|
||
}
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 每个 API 端点都有认证和授权检查
|
||
- [ ] 资源访问时校验所有者身份(防止 IDOR)
|
||
- [ ] 默认拒绝策略:未明确授权的请求一律拒绝
|
||
- [ ] CORS 仅允许受信任的来源
|
||
- [ ] 目录列表已禁用,`.git` / `.env` 等文件不可访问
|
||
|
||
---
|
||
|
||
## A02: 加密失效 (Cryptographic Failures)
|
||
|
||
### 风险描述
|
||
|
||
敏感数据(密码、信用卡号、个人信息)未加密或使用弱加密算法。包括:明文传输、弱哈希算法(MD5/SHA1)、密钥硬编码、缺少 TLS。
|
||
|
||
### 攻击示例
|
||
|
||
```python
|
||
# 错误:使用 MD5 存储密码
|
||
import hashlib
|
||
password_hash = hashlib.md5(password.encode()).hexdigest() # 极易被彩虹表破解
|
||
|
||
# 错误:密钥硬编码
|
||
SECRET_KEY = "my-secret-key-123" # 泄露到版本控制
|
||
```
|
||
|
||
### FastAPI 防护代码
|
||
|
||
```python
|
||
# 密码哈希 — 推荐 Argon2
|
||
from argon2 import PasswordHasher
|
||
from argon2.exceptions import VerifyMismatchError
|
||
|
||
ph = PasswordHasher(
|
||
time_cost=3, # 迭代次数
|
||
memory_cost=65536, # 内存消耗 64MB
|
||
parallelism=4, # 并行线程
|
||
hash_len=32, # 哈希长度
|
||
salt_len=16, # 盐长度
|
||
)
|
||
|
||
def hash_password(password: str) -> str:
|
||
return ph.hash(password)
|
||
|
||
def verify_password(password: str, hashed: str) -> bool:
|
||
try:
|
||
return ph.verify(hashed, password)
|
||
except VerifyMismatchError:
|
||
return False
|
||
|
||
# 数据加密 — AES-256-GCM
|
||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||
import os
|
||
|
||
def encrypt_sensitive_data(plaintext: bytes, key: bytes) -> bytes:
|
||
"""使用 AES-256-GCM 加密敏感数据"""
|
||
nonce = os.urandom(12) # 96-bit nonce
|
||
aesgcm = AESGCM(key)
|
||
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
|
||
return nonce + ciphertext # nonce 拼接密文
|
||
|
||
def decrypt_sensitive_data(data: bytes, key: bytes) -> bytes:
|
||
nonce = data[:12]
|
||
ciphertext = data[12:]
|
||
aesgcm = AESGCM(key)
|
||
return aesgcm.decrypt(nonce, ciphertext, None)
|
||
|
||
# TLS 配置 — 生产环境使用 TLS 1.3
|
||
# uvicorn main:app --ssl-keyfile=key.pem --ssl-certfile=cert.pem --ssl-version=TLSv1.3
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 密码使用 Argon2 或 bcrypt 哈希存储
|
||
- [ ] 敏感数据使用 AES-256-GCM 加密
|
||
- [ ] 传输层强制 TLS 1.2+,推荐 TLS 1.3
|
||
- [ ] 密钥通过环境变量或密钥管理服务注入,不硬编码
|
||
- [ ] 禁用 MD5、SHA1 等弱哈希算法
|
||
|
||
---
|
||
|
||
## A03: 注入 (Injection)
|
||
|
||
### 风险描述
|
||
|
||
攻击者将恶意数据作为命令或查询的一部分发送。常见类型包括 SQL 注入、NoSQL 注入、OS Command 注入、LDAP 注入。
|
||
|
||
### 攻击示例
|
||
|
||
```python
|
||
# SQL 注入
|
||
query = f"SELECT * FROM users WHERE name = '{user_input}'"
|
||
# 输入: ' OR '1'='1' -- → 返回所有用户
|
||
|
||
# OS Command 注入
|
||
os.system(f"ping {user_input}")
|
||
# 输入: 127.0.0.1; rm -rf / → 执行恶意命令
|
||
```
|
||
|
||
### FastAPI 防护代码
|
||
|
||
```python
|
||
# SQL 注入防护 — 使用 ORM(SQLAlchemy)
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import text
|
||
|
||
# 正确:使用 ORM 查询
|
||
def get_user_by_name(db: Session, name: str):
|
||
return db.query(User).filter(User.name == name).first()
|
||
|
||
# 正确:使用参数化查询
|
||
def search_users(db: Session, keyword: str):
|
||
stmt = text("SELECT * FROM users WHERE name LIKE :keyword")
|
||
return db.execute(stmt, {"keyword": f"%{keyword}%"}).fetchall()
|
||
|
||
# 错误示范(绝不使用):
|
||
# db.execute(f"SELECT * FROM users WHERE name = '{name}'")
|
||
|
||
# OS Command 注入防护 — 使用 subprocess 参数列表
|
||
import subprocess
|
||
import shlex
|
||
|
||
def safe_ping(host: str):
|
||
# 输入验证
|
||
import re
|
||
if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
|
||
raise ValueError("无效的主机名")
|
||
# 使用参数列表而非字符串拼接
|
||
result = subprocess.run(
|
||
["ping", "-c", "4", host],
|
||
capture_output=True, text=True, timeout=10
|
||
)
|
||
return result.stdout
|
||
|
||
# NoSQL 注入防护 — MongoDB
|
||
from bson import ObjectId
|
||
|
||
async def get_user(user_id: str):
|
||
# 验证 ObjectId 格式
|
||
if not ObjectId.is_valid(user_id):
|
||
raise HTTPException(status_code=400, detail="无效的用户 ID")
|
||
return await collection.find_one({"_id": ObjectId(user_id)})
|
||
|
||
# 输入验证 — Pydantic 模型
|
||
from pydantic import BaseModel, validator
|
||
import re
|
||
|
||
class UserSearch(BaseModel):
|
||
keyword: str
|
||
|
||
@validator("keyword")
|
||
def sanitize_keyword(cls, v):
|
||
if not re.match(r'^[\w\s\u4e00-\u9fff]+$', v):
|
||
raise ValueError("搜索关键词包含非法字符")
|
||
if len(v) > 100:
|
||
raise ValueError("搜索关键词过长")
|
||
return v.strip()
|
||
```
|
||
|
||
### Next.js 防护代码
|
||
|
||
```typescript
|
||
// API Route 输入验证 — 使用 zod
|
||
import { z } from "zod";
|
||
|
||
const searchSchema = z.object({
|
||
keyword: z.string().min(1).max(100).regex(/^[\w\s\u4e00-\u9fff]+$/),
|
||
page: z.number().int().positive().default(1),
|
||
});
|
||
|
||
export async function GET(request: Request) {
|
||
const { searchParams } = new URL(request.url);
|
||
const result = searchSchema.safeParse({
|
||
keyword: searchParams.get("keyword"),
|
||
page: Number(searchParams.get("page")),
|
||
});
|
||
|
||
if (!result.success) {
|
||
return Response.json({ error: "参数验证失败" }, { status: 400 });
|
||
}
|
||
|
||
// 使用 Prisma ORM 查询(自动防 SQL 注入)
|
||
const users = await prisma.user.findMany({
|
||
where: { name: { contains: result.data.keyword } },
|
||
skip: (result.data.page - 1) * 20,
|
||
take: 20,
|
||
});
|
||
return Response.json(users);
|
||
}
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 所有 SQL 使用 ORM 或参数化查询
|
||
- [ ] 系统命令使用 subprocess 参数列表,禁止字符串拼接
|
||
- [ ] 所有用户输入使用 Pydantic/zod 校验
|
||
- [ ] NoSQL 查询验证数据类型和格式
|
||
- [ ] HTML 输出自动转义(React/Jinja2 默认行为)
|
||
|
||
---
|
||
|
||
## A04: 不安全设计 (Insecure Design)
|
||
|
||
### 风险描述
|
||
|
||
设计层面的安全缺陷,无法通过代码修补解决。包括缺少威胁建模、业务逻辑漏洞、缺乏速率限制。
|
||
|
||
### 防护策略
|
||
|
||
```python
|
||
# 速率限制 — 使用 slowapi
|
||
from slowapi import Limiter
|
||
from slowapi.util import get_remote_address
|
||
|
||
limiter = Limiter(key_func=get_remote_address)
|
||
|
||
@app.post("/api/auth/login")
|
||
@limiter.limit("5/minute") # 每分钟最多 5 次登录尝试
|
||
async def login(request: Request, credentials: LoginForm):
|
||
# 登录逻辑
|
||
pass
|
||
|
||
# 业务逻辑保护 — 幂等性检查
|
||
from fastapi import Header
|
||
|
||
@app.post("/api/orders")
|
||
async def create_order(
|
||
order: OrderCreate,
|
||
idempotency_key: str = Header(...),
|
||
current_user: User = Depends(get_current_user),
|
||
):
|
||
# 幂等性检查:防止重复提交
|
||
existing = await db.get_order_by_idempotency_key(idempotency_key)
|
||
if existing:
|
||
return existing
|
||
return await db.create_order(order, current_user.id, idempotency_key)
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 关键业务流程已进行威胁建模(STRIDE)
|
||
- [ ] 敏感操作有速率限制
|
||
- [ ] 支付/转账等操作有幂等性保护
|
||
- [ ] 业务规则在服务端验证,不依赖前端
|
||
|
||
---
|
||
|
||
## A05: 安全配置错误 (Security Misconfiguration)
|
||
|
||
### 风险描述
|
||
|
||
默认配置、不完整配置、开放的云存储、不必要的 HTTP 头、详细的错误信息泄露敏感信息。
|
||
|
||
### FastAPI 防护代码
|
||
|
||
```python
|
||
from fastapi import FastAPI
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
|
||
app = FastAPI(
|
||
docs_url=None if IS_PRODUCTION else "/docs", # 生产环境禁用文档
|
||
redoc_url=None if IS_PRODUCTION else "/redoc",
|
||
openapi_url=None if IS_PRODUCTION else "/openapi.json",
|
||
)
|
||
|
||
# CORS 严格配置
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["https://your-domain.com"], # 不使用 ["*"]
|
||
allow_credentials=True,
|
||
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
||
allow_headers=["Authorization", "Content-Type"],
|
||
)
|
||
|
||
# 全局异常处理 — 隐藏内部错误细节
|
||
@app.exception_handler(Exception)
|
||
async def global_exception_handler(request, exc):
|
||
logger.error(f"未处理异常: {exc}", exc_info=True)
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={"detail": "服务器内部错误"}, # 不暴露堆栈信息
|
||
)
|
||
```
|
||
|
||
### 加固清单
|
||
|
||
- [ ] 生产环境禁用调试模式和 API 文档
|
||
- [ ] CORS 配置白名单模式,不使用通配符
|
||
- [ ] 异常处理器隐藏内部错误详情
|
||
- [ ] 移除 Server/X-Powered-By 等信息泄露头
|
||
- [ ] 文件上传限制大小和类型
|
||
- [ ] 禁用不必要的 HTTP 方法(TRACE、OPTIONS)
|
||
|
||
---
|
||
|
||
## A06: 易受攻击和过时组件 (Vulnerable and Outdated Components)
|
||
|
||
### 风险描述
|
||
|
||
使用存在已知漏洞的库、框架或组件。依赖链中的任何环节都可能引入风险。
|
||
|
||
### 依赖扫描工具
|
||
|
||
```bash
|
||
# Python 依赖扫描
|
||
pip install pip-audit
|
||
pip-audit # 扫描已安装的包
|
||
pip-audit -r requirements.txt # 扫描依赖文件
|
||
|
||
# 使用 safety 扫描
|
||
pip install safety
|
||
safety check
|
||
|
||
# Node.js 依赖扫描
|
||
npm audit # 内置审计
|
||
npm audit fix # 自动修复
|
||
npx audit-ci --moderate # CI 集成,中等以上阻断
|
||
|
||
# pnpm 依赖扫描
|
||
pnpm audit
|
||
pnpm audit --fix
|
||
|
||
# 通用工具 — Trivy
|
||
trivy fs . # 扫描项目目录
|
||
trivy image myapp:latest # 扫描 Docker 镜像
|
||
```
|
||
|
||
### CI/CD 集成
|
||
|
||
```yaml
|
||
# GitHub Actions — 依赖扫描
|
||
- name: Python 依赖审计
|
||
run: |
|
||
pip install pip-audit
|
||
pip-audit -r requirements.txt --strict
|
||
|
||
- name: Node.js 依赖审计
|
||
run: pnpm audit --audit-level=moderate
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] CI/CD 流水线集成依赖扫描
|
||
- [ ] 定期更新依赖版本(至少每月一次)
|
||
- [ ] 使用 Dependabot / Renovate 自动化依赖更新
|
||
- [ ] 监控 CVE 公告(NVD、GitHub Advisory)
|
||
|
||
---
|
||
|
||
## A07: 身份识别和认证失败 (Identification and Authentication Failures)
|
||
|
||
### 风险描述
|
||
|
||
弱密码策略、缺乏暴力破解保护、未实现 MFA、Session 固定攻击。
|
||
|
||
### FastAPI 防护代码
|
||
|
||
```python
|
||
# 密码复杂度验证
|
||
import re
|
||
from pydantic import BaseModel, validator
|
||
|
||
class PasswordPolicy(BaseModel):
|
||
password: str
|
||
|
||
@validator("password")
|
||
def validate_strength(cls, v):
|
||
if len(v) < 12:
|
||
raise ValueError("密码长度至少 12 位")
|
||
if not re.search(r'[A-Z]', v):
|
||
raise ValueError("需要至少一个大写字母")
|
||
if not re.search(r'[a-z]', v):
|
||
raise ValueError("需要至少一个小写字母")
|
||
if not re.search(r'\d', v):
|
||
raise ValueError("需要至少一个数字")
|
||
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', v):
|
||
raise ValueError("需要至少一个特殊字符")
|
||
return v
|
||
|
||
# 暴力破解防护 — 账户锁定
|
||
from datetime import datetime, timedelta
|
||
|
||
class LoginAttemptTracker:
|
||
MAX_ATTEMPTS = 5
|
||
LOCKOUT_DURATION = timedelta(minutes=15)
|
||
|
||
async def check_and_record(self, username: str, success: bool):
|
||
key = f"login_attempts:{username}"
|
||
attempts = await redis.get(key)
|
||
|
||
if attempts and int(attempts) >= self.MAX_ATTEMPTS:
|
||
ttl = await redis.ttl(key)
|
||
raise HTTPException(
|
||
status_code=429,
|
||
detail=f"账户已锁定,请在 {ttl} 秒后重试"
|
||
)
|
||
|
||
if not success:
|
||
await redis.incr(key)
|
||
await redis.expire(key, int(self.LOCKOUT_DURATION.total_seconds()))
|
||
else:
|
||
await redis.delete(key)
|
||
|
||
# TOTP 双因素认证
|
||
import pyotp
|
||
|
||
def generate_totp_secret() -> str:
|
||
return pyotp.random_base32()
|
||
|
||
def verify_totp(secret: str, code: str) -> bool:
|
||
totp = pyotp.TOTP(secret)
|
||
return totp.verify(code, valid_window=1) # 允许前后 30 秒偏移
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 密码策略:最少 12 位,含大小写、数字、特殊字符
|
||
- [ ] 暴力破解防护:5 次失败后锁定 15 分钟
|
||
- [ ] 敏感操作启用 MFA(TOTP/WebAuthn)
|
||
- [ ] Session ID 在登录后重新生成(防 Session 固定)
|
||
- [ ] 使用安全的密码重置流程(带过期的一次性令牌)
|
||
|
||
---
|
||
|
||
## A08: 软件和数据完整性失败 (Software and Data Integrity Failures)
|
||
|
||
### 风险描述
|
||
|
||
软件更新、CI/CD 管道和反序列化缺乏完整性验证。供应链攻击正在成为主要威胁。
|
||
|
||
### 防护策略
|
||
|
||
```bash
|
||
# npm 锁文件完整性 — 使用 --frozen-lockfile
|
||
pnpm install --frozen-lockfile # CI 中确保使用锁文件
|
||
|
||
# Python 依赖哈希校验
|
||
pip install --require-hashes -r requirements.txt
|
||
|
||
# requirements.txt 带哈希
|
||
# flask==3.0.0 --hash=sha256:xxxx
|
||
```
|
||
|
||
```python
|
||
# 反序列化安全 — 禁止 pickle 反序列化不可信数据
|
||
import json
|
||
|
||
# 正确:使用 JSON
|
||
data = json.loads(user_input)
|
||
|
||
# 错误:不要对不可信数据使用 pickle
|
||
# import pickle
|
||
# data = pickle.loads(user_input) # 远程代码执行风险!
|
||
|
||
# Webhook 签名验证
|
||
import hmac
|
||
import hashlib
|
||
|
||
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
|
||
expected = hmac.new(
|
||
secret.encode(), payload, hashlib.sha256
|
||
).hexdigest()
|
||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] CI/CD 使用锁文件和哈希校验
|
||
- [ ] 禁止反序列化不可信数据(pickle/yaml.load)
|
||
- [ ] Webhook 回调验证签名
|
||
- [ ] Docker 镜像使用固定摘要而非 latest 标签
|
||
- [ ] 代码签名和发布流程有完整性校验
|
||
|
||
---
|
||
|
||
## A09: 安全日志和监控失败 (Security Logging and Monitoring Failures)
|
||
|
||
### 风险描述
|
||
|
||
缺乏充分的日志记录和监控使攻击难以被检测。平均攻击检测时间超过 200 天。
|
||
|
||
### FastAPI 安全日志
|
||
|
||
```python
|
||
import logging
|
||
import structlog
|
||
from datetime import datetime
|
||
|
||
# 结构化安全日志
|
||
security_logger = structlog.get_logger("security")
|
||
|
||
# 记录认证事件
|
||
async def log_auth_event(event_type: str, username: str, ip: str, success: bool, detail: str = ""):
|
||
security_logger.info(
|
||
"auth_event",
|
||
event_type=event_type,
|
||
username=username,
|
||
ip_address=ip,
|
||
success=success,
|
||
detail=detail,
|
||
timestamp=datetime.utcnow().isoformat(),
|
||
)
|
||
|
||
# 记录访问控制事件
|
||
async def log_access_event(user_id: int, resource: str, action: str, allowed: bool):
|
||
security_logger.info(
|
||
"access_event",
|
||
user_id=user_id,
|
||
resource=resource,
|
||
action=action,
|
||
allowed=allowed,
|
||
timestamp=datetime.utcnow().isoformat(),
|
||
)
|
||
|
||
# 关键:不要在日志中记录敏感数据
|
||
# 错误:logger.info(f"用户登录: password={password}")
|
||
# 正确:logger.info(f"用户登录: username={username}")
|
||
```
|
||
|
||
### 告警规则示例
|
||
|
||
```yaml
|
||
# Prometheus 告警规则
|
||
groups:
|
||
- name: security_alerts
|
||
rules:
|
||
- alert: BruteForceDetected
|
||
expr: rate(auth_failures_total[5m]) > 10
|
||
for: 1m
|
||
annotations:
|
||
summary: "检测到暴力破解攻击"
|
||
|
||
- alert: UnauthorizedAccessSpike
|
||
expr: rate(http_responses_total{status="403"}[5m]) > 20
|
||
for: 2m
|
||
annotations:
|
||
summary: "403 响应异常增多"
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 记录所有认证事件(登录、登出、失败)
|
||
- [ ] 记录访问控制失败事件
|
||
- [ ] 日志中不包含密码、令牌等敏感信息
|
||
- [ ] 日志集中存储且防篡改
|
||
- [ ] 配置告警规则:暴力破解、异常访问模式
|
||
|
||
---
|
||
|
||
## A10: 服务器端请求伪造 (Server-Side Request Forgery, SSRF)
|
||
|
||
### 风险描述
|
||
|
||
攻击者让服务器发起非预期的请求,访问内部服务、云元数据端点或内网资源。
|
||
|
||
### 攻击示例
|
||
|
||
```
|
||
# 访问云实例元数据
|
||
POST /api/fetch-url
|
||
{"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"}
|
||
|
||
# 访问内网服务
|
||
{"url": "http://192.168.1.100:6379/"} # 直接访问内网 Redis
|
||
```
|
||
|
||
### FastAPI 防护代码
|
||
|
||
```python
|
||
import ipaddress
|
||
from urllib.parse import urlparse
|
||
import socket
|
||
|
||
ALLOWED_SCHEMES = {"http", "https"}
|
||
BLOCKED_NETWORKS = [
|
||
ipaddress.ip_network("10.0.0.0/8"),
|
||
ipaddress.ip_network("172.16.0.0/12"),
|
||
ipaddress.ip_network("192.168.0.0/16"),
|
||
ipaddress.ip_network("127.0.0.0/8"),
|
||
ipaddress.ip_network("169.254.0.0/16"), # 云元数据
|
||
ipaddress.ip_network("0.0.0.0/8"),
|
||
]
|
||
|
||
def validate_url(url: str) -> str:
|
||
"""验证 URL 安全性,防止 SSRF"""
|
||
parsed = urlparse(url)
|
||
|
||
# 协议白名单
|
||
if parsed.scheme not in ALLOWED_SCHEMES:
|
||
raise ValueError(f"不允许的协议: {parsed.scheme}")
|
||
|
||
# 域名白名单(推荐)
|
||
ALLOWED_DOMAINS = ["api.example.com", "cdn.example.com"]
|
||
if parsed.hostname not in ALLOWED_DOMAINS:
|
||
raise ValueError(f"不允许的域名: {parsed.hostname}")
|
||
|
||
# 解析 IP 并检查是否为内网地址
|
||
try:
|
||
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
|
||
for network in BLOCKED_NETWORKS:
|
||
if ip in network:
|
||
raise ValueError(f"不允许访问内网地址: {ip}")
|
||
except socket.gaierror:
|
||
raise ValueError(f"无法解析域名: {parsed.hostname}")
|
||
|
||
return url
|
||
|
||
@app.post("/api/fetch-url")
|
||
async def fetch_external_url(url: str):
|
||
safe_url = validate_url(url)
|
||
async with httpx.AsyncClient(timeout=10) as client:
|
||
response = await client.get(safe_url, follow_redirects=False)
|
||
return {"status": response.status_code, "body": response.text[:1000]}
|
||
```
|
||
|
||
### 检查清单
|
||
|
||
- [ ] 用户提供的 URL 进行协议白名单检查
|
||
- [ ] DNS 解析结果检查,阻止内网 IP
|
||
- [ ] 使用域名白名单而非黑名单
|
||
- [ ] 禁止 HTTP 重定向跟踪(或重定向后再次验证)
|
||
- [ ] 云环境中使用 IMDSv2 或禁用实例元数据
|
||
|
||
---
|
||
|
||
## 快速参考表
|
||
|
||
| 编号 | 名称 | 核心防护 |
|
||
|------|------|----------|
|
||
| A01 | 访问控制失效 | RBAC + 资源归属校验 + 默认拒绝 |
|
||
| A02 | 加密失效 | Argon2 + AES-256-GCM + TLS 1.3 |
|
||
| A03 | 注入 | ORM + 参数化查询 + 输入验证 |
|
||
| A04 | 不安全设计 | 威胁建模 + 速率限制 + 幂等性 |
|
||
| A05 | 安全配置错误 | 生产加固 + CORS 白名单 + 错误隐藏 |
|
||
| A06 | 易受攻击组件 | pip-audit + npm audit + CI 集成 |
|
||
| A07 | 认证失败 | 强密码策略 + 账户锁定 + MFA |
|
||
| A08 | 数据完整性失败 | 锁文件 + 签名验证 + 禁止 pickle |
|
||
| A09 | 日志监控不足 | 结构化日志 + 告警规则 + 集中存储 |
|
||
| A10 | SSRF | URL 白名单 + IP 检查 + 禁止重定向 |
|