> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Web 安全基础
- URL: https://blog.vercanti.com/web-an-quan-ji-chu/
- Published: 2026-08-28T14:35:47.000Z
- Updated: 2026-08-28T14:59:36.000Z
- Description: 1. HTML 转义（服务端渲染必须）： 2. CSP（Content Security Policy）： 3. HttpOnly Cookie：防止 JS 读取会话 Cookie： 4. 前端框架防护： 攻击者在第三方网站构造一个表单，诱导已登录用户提交，浏览器会自动携带目标站的 Cookie。 触发条件： 1. 用户已登录目标网站（Cookie 有效） 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token（推荐）： 2. SameSite Cookie： 3. 验证 Origin/Referer 头：
- Author: yellowdog
- Tags: 网络安全

> 官方文档：<https://owasp.org/www-project-top-ten/>  
> 适用版本：OWASP Top 10 2021（2026-05-07 核实）

---

## OWASP Top 10 概览

| 排名  | 漏洞类型          | 简述                 |
| --- | ------------- | ------------------ |
| A01 | 访问控制失效        | 用户可访问未授权的资源        |
| A02 | 加密机制失效        | 敏感数据明文传输/存储        |
| A03 | 注入            | SQL 注入、命令注入、XSS 等  |
| A04 | 不安全设计         | 设计阶段缺乏威胁建模         |
| A05 | 安全配置错误        | 默认密码、错误信息泄露        |
| A06 | 自带缺陷和过时的组件    | 使用有漏洞的依赖库          |
| A07 | 认证和验证机制失效     | 弱密码、会话固定           |
| A08 | 软件和数据完整性失效    | 反序列化漏洞、CI/CD 供应链攻击 |
| A09 | 安全日志和监控失效     | 无法检测攻击、响应迟缓        |
| A10 | 服务端请求伪造（SSRF） | 服务器被迫发请求到内网        |

---

## XSS（跨站脚本攻击）

### 攻击类型

| 类型       | 原理                      | 典型场景                |
| -------- | ----------------------- | ------------------- |
| 存储型（持久型） | 恶意脚本存入数据库，每次渲染时执行       | 评论区、用户资料            |
| 反射型（非持久） | 恶意脚本包含在 URL 参数中，服务端直接返回 | 搜索页、错误页             |
| DOM 型    | 客户端 JS 直接操作 DOM，未经过服务端  | 前端路由 hash、innerHTML |

### 防御措施

**1\. HTML 转义**（服务端渲染必须）：

```python
import html

user_input = '<script>alert("xss")</script>'
safe_output = html.escape(user_input)
# 输出：&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;

```

**2\. CSP（Content Security Policy）**：

```python
# FastAPI 添加 CSP 头
from fastapi import Response

@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self' 'nonce-{nonce}'; "
        "style-src 'self' 'unsafe-inline';"
    )
    return response

```

**3\. HttpOnly Cookie**：防止 JS 读取会话 Cookie：

```python
response.set_cookie(
    key="session_id",
    value=session_token,
    httponly=True,   # 禁止 JS 访问
    secure=True,     # 仅 HTTPS 传输
    samesite="strict"
)

```

**4\. 前端框架防护**：

- React：`dangerouslySetInnerHTML` 要经过 `DOMPurify.sanitize()`
- Vue：`v-html` 同上，避免直接渲染用户内容

---

## SQL 注入

### 攻击原理

```python
# 危险写法：字符串拼接
user_id = request.query_params["id"]
query = f"SELECT * FROM users WHERE id = {user_id}"
# 攻击者传入：1 OR 1=1
# 实际执行：SELECT * FROM users WHERE id = 1 OR 1=1

```

### 防御：参数化查询

```python
# SQLAlchemy ORM（天然安全）
user = await User.get(id=user_id)

# SQLAlchemy Core 参数化
from sqlalchemy import text
result = await db.execute(
    text("SELECT * FROM users WHERE id = :id"),
    {"id": user_id}
)

# aiomysql 参数化
await cursor.execute(
    "SELECT * FROM users WHERE id = %s",
    (user_id,)
)

```

### 检测工具

- sqlmap：自动化 SQL 注入检测
- 手工测试：输入 `'`、`1 OR 1=1`、`1; DROP TABLE users--` 观察响应

---

## CSRF（跨站请求伪造）

### 攻击原理

攻击者在第三方网站构造一个表单，诱导已登录用户提交，浏览器会自动携带目标站的 Cookie。

**触发条件**：

1. 用户已登录目标网站（Cookie 有效）
2. 目标 API 仅凭 Cookie 识别用户身份
3. 请求来源未验证

### 防御方案

**1\. CSRF Token（推荐）**：

```python
import secrets
from fastapi import Request, HTTPException

def generate_csrf_token() -> str:
    return secrets.token_urlsafe(32)

async def verify_csrf_token(request: Request):
    """依赖注入：验证 CSRF Token"""
    token_from_header = request.headers.get("X-CSRF-Token")
    token_from_session = request.session.get("csrf_token")
    if not token_from_header or token_from_header != token_from_session:
        raise HTTPException(status_code=403, detail="CSRF token invalid")

```

**2\. SameSite Cookie**：

```python
response.set_cookie(
    key="session_id",
    value=session_token,
    samesite="strict",  # 第三方站点请求不携带此 Cookie
)

```

| SameSite 值 | 行为                         |
| ---------- | -------------------------- |
| Strict     | 只有同站请求才携带 Cookie（最严格）      |
| Lax        | GET 导航可携带，POST 等不行（浏览器默认值） |
| None       | 总是携带，必须同时设置 Secure=True    |

**3\. 验证 Origin/Referer 头**：

```python
ALLOWED_ORIGINS = {"https://example.com", "https://www.example.com"}

async def check_origin(request: Request):
    origin = request.headers.get("Origin") or request.headers.get("Referer", "")
    if not any(origin.startswith(allowed) for allowed in ALLOWED_ORIGINS):
        raise HTTPException(status_code=403)

```

---

## 认证与授权

### 密码存储

**永远不要明文存储密码，不要用 MD5/SHA1：**

```python
# 推荐：bcrypt 或 argon2
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(plain: str) -> str:
    return pwd_context.hash(plain)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

```

**bcrypt vs argon2 对比**：

| 算法       | 内存硬度 | 抗 GPU | 推荐度                                    |
| -------- | ---- | ----- | -------------------------------------- |
| bcrypt   | 否    | 一般    | 广泛使用，稳定                                |
| argon2id | 是    | 强     | 新项目推荐（Password Hashing Competition 冠军） |
| PBKDF2   | 否    | 弱     | 仅用于合规要求                                |

### JWT 安全使用

参考 [JWT完全指南](https://blog.vercanti.com/jwt-wan-quan-zhi-nan/)，关键安全要点：

| 要点                | 说明                                          |
| ----------------- | ------------------------------------------- |
| 算法指定              | 验证时必须指定 algorithms=\["HS256"\]，禁止 alg: none |
| Access Token 短有效期 | 建议 15 分钟到 1 小时                              |
| Refresh Token 轮换  | 每次使用后颁发新的 Refresh Token                     |
| 黑名单               | 登出时将 Token 加入 Redis 黑名单                     |
| 敏感信息              | JWT Payload 不存放密码等敏感信息（Base64 可解码）          |

### OAuth2 授权码流程

```
1. 用户点击"第三方登录"→ 跳转到授权服务器（带 client_id + redirect_uri + state）
2. 用户在授权服务器登录并同意授权
3. 授权服务器重定向回 redirect_uri，携带 code（授权码，一次性）
4. 后端用 code + client_secret 向授权服务器换取 access_token
5. 用 access_token 调用资源服务器获取用户信息

```

**state 参数**：防止 CSRF 攻击，客户端生成随机值，回调时验证是否一致。

---

## SSRF（服务端请求伪造）

### 危害

攻击者控制服务端发起的请求，可访问：

- 内网服务（数据库、Redis、内部 API）
- 云服务商元数据接口（`http://169.254.169.254`，获取 IAM 凭证）

### 防御

```python
import ipaddress
import socket
from urllib.parse import urlparse

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"),  # 云元数据
]

def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        return False
    try:
        ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
    except Exception:
        return False
    return not any(ip in net for net in BLOCKED_NETWORKS)

async def fetch_external(url: str) -> bytes:
    if not is_safe_url(url):
        raise ValueError("URL not allowed")
    async with httpx.AsyncClient() as client:
        response = await client.get(url, timeout=5)
        return response.content

```

---

## 文件上传安全

| 校验项  | 实现方式                                 |
| ---- | ------------------------------------ |
| 文件类型 | 读取文件头魔术字节，不信任 Content-Type 和扩展名      |
| 文件大小 | 限制最大上传大小（FastAPI UploadFile \+ 中间件）  |
| 文件名  | 重命名为随机 UUID，防止路径穿越（../../etc/passwd） |
| 存储位置 | 存储在 Web 根目录之外或 OSS，禁止直接执行            |
| 病毒扫描 | 可选：ClamAV 集成                         |

```python
import uuid
import magic  # python-magic
from pathlib import Path

ALLOWED_MIME_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
MAX_SIZE = 5 * 1024 * 1024  # 5MB

async def save_upload(file: UploadFile) -> str:
    content = await file.read()

    # 检查大小
    if len(content) > MAX_SIZE:
        raise ValueError("File too large")

    # 检查真实 MIME 类型（读魔术字节）
    mime = magic.from_buffer(content, mime=True)
    if mime not in ALLOWED_MIME_TYPES:
        raise ValueError(f"File type not allowed: {mime}")

    # 随机文件名
    ext = {"image/jpeg": ".jpg", "image/png": ".png",
           "image/gif": ".gif", "image/webp": ".webp"}[mime]
    filename = f"{uuid.uuid4().hex}{ext}"

    upload_dir = Path("/var/uploads")  # Web 根目录之外
    upload_dir.mkdir(parents=True, exist_ok=True)
    (upload_dir / filename).write_bytes(content)

    return filename

```

---

## HTTPS 配置安全

### HSTS（强制 HTTPS）

```nginx
# Nginx 配置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

```

```python
# FastAPI 中间件
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)

```

### 禁用弱密码套件（Nginx）

```nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

```

### 安全响应头速查

| 响应头                     | 推荐值                             | 作用            |
| ----------------------- | ------------------------------- | ------------- |
| X-Content-Type-Options  | nosniff                         | 禁止 MIME 嗅探    |
| X-Frame-Options         | DENY 或 SAMEORIGIN               | 防止点击劫持        |
| Referrer-Policy         | strict-origin-when-cross-origin | 控制 Referer 信息 |
| Permissions-Policy      | geolocation=(), camera=()       | 限制浏览器 API 权限  |
| Content-Security-Policy | 见 XSS 节                         | 防 XSS         |

---

## 最佳实践

### 纵深防御原则

安全不能依赖单一防线。每个层次都要设置独立防御：

| 层次  | 防御措施                  |
| --- | --------------------- |
| 网络层 | WAF、DDoS 防护、IP 白名单    |
| 传输层 | HTTPS / TLS 1.2+，HSTS |
| 应用层 | 输入校验、参数化查询、CSP        |
| 认证层 | 强密码策略、MFA、短会话         |
| 数据层 | 加密静态数据、字段级权限          |
| 监控层 | 异常登录检测、审计日志           |

### 输入验证三原则

1. **在服务端验证**：永远不要仅依赖前端校验，所有用户输入都必须在服务端再次验证
2. **白名单而非黑名单**：明确允许什么格式，而不是过滤已知危险字符
3. **最小化信任**：即使是内部服务之间的请求也要验证身份

```python
from pydantic import BaseModel, constr, validator
import re

class UserCreateRequest(BaseModel):
    username: constr(min_length=3, max_length=32, pattern=r'^[a-zA-Z0-9_]+$')
    email: str
    age: int

    @validator('email')
    def validate_email(cls, v):
        if not re.match(r'^[^@]+@[^@]+\.[^@]+$', v):
            raise ValueError('Invalid email format')
        return v.lower()

```

### 密钥与凭证管理

```python
# 错误：硬编码密钥
SECRET_KEY = "mysecretkey123"

# 正确：从环境变量读取
import os
SECRET_KEY = os.environ["SECRET_KEY"]  # 启动时若缺失会立即报错

# 生产环境推荐：HashiCorp Vault / AWS Secrets Manager / GCP Secret Manager

```

- 使用 `.env` 文件存储本地开发密钥，`.env` 加入 `.gitignore`
- 定期轮换密钥（JWT secret、API key、数据库密码）
- 不同环境（dev/staging/prod）使用不同密钥

### 安全编码检查清单

| 检查项      | 工具/方法                              |
| -------- | ---------------------------------- |
| 依赖漏洞扫描   | pip audit、safety check、Snyk        |
| SQL 注入防护 | 使用 ORM 或参数化查询，禁止字符串拼接              |
| 敏感信息泄露   | truffleHog / git-secrets 扫描 Git 历史 |
| 弱密码策略    | 强制最小长度 12 位，含大小写+数字+符号             |
| 权限最小化    | 数据库用户只给 SELECT/INSERT，不给 DROP      |
| 错误处理     | 生产环境返回通用错误，详情只写日志                  |

---

## 常见陷阱

### 陷阱：SQL 拼接字符串导致注入漏洞

**现象：** 用户输入 `' OR '1'='1` 绕过登录验证，或输入 `'; DROP TABLE users; --` 删除数据。

**原因：** 将用户输入直接拼接进 SQL 字符串，攻击者可以改变 SQL 语义。

**解决：** 始终使用参数化查询（ORM 或 `cursor.execute(sql, params)`），永远不要用字符串格式化拼接 SQL。

```python
# 错误：字符串拼接，存在注入风险
cursor.execute(f"SELECT * FROM users WHERE name = '{username}'")

# 正确：参数化查询
cursor.execute("SELECT * FROM users WHERE name = %s", (username,))

```

### 陷阱：生产环境开启 debug 模式暴露内部信息

**现象：** 接口报错时，响应体包含完整的 Python 堆栈、数据库连接字符串、本地文件路径等敏感信息。

**原因：** 框架的 debug 模式（FastAPI `debug=True`、Django `DEBUG=True`）会在响应中返回详细错误信息，方便开发调试，但在生产环境会暴露内部实现。

**解决：** 生产环境通过环境变量控制 debug 开关，错误响应只返回通用错误码和 ID，详细错误记录到日志（不包含敏感字段）。

### 陷阱：JWT Secret 使用弱密钥或硬编码

**现象：** 攻击者通过暴力破解或代码泄露获得 JWT Secret，伪造任意用户的 Token，绕过认证。

**原因：** 使用简短密钥（如 `secret`、`123456`）容易被字典攻击；硬编码在源码中一旦仓库泄露立即失效。

**解决：** JWT Secret 至少 256 位随机字节（`openssl rand -hex 32`），通过环境变量注入，定期轮换；对高价值接口添加额外的时效校验（不依赖 JWT `exp` 单点失效）。

---

## 参见

- [JWT完全指南](https://blog.vercanti.com/jwt-wan-quan-zhi-nan/) — JWT 认证的安全实现与常见漏洞
- [HTTP协议深度指南](https://blog.vercanti.com/http-xie-yi-shen-du-zhi-nan/) — HTTPS、Cookie 安全属性、安全响应头详解
- [FastAPI完全指南](https://blog.vercanti.com/fastapi-wan-quan-zhi-nan/) — FastAPI 安全最佳实践（认证、权限、依赖注入）