Web 安全基础

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 头:

分享

官方文档: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 转义(服务端渲染必须):

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)

# 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:

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 注入

攻击原理

# 危险写法:字符串拼接
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

防御:参数化查询

# 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=11; DROP TABLE users-- 观察响应

CSRF(跨站请求伪造)

攻击原理

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

触发条件

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

防御方案

1. CSRF Token(推荐)

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

response.set_cookie(
    key="session_id",
    value=session_token,
    samesite="strict",  # 第三方站点请求不携带此 Cookie
)
SameSite 值 行为
Strict 只有同站请求才携带 Cookie(最严格)
Lax GET 导航可携带,POST 等不行(浏览器默认值)
None 总是携带,必须同时设置 Secure=True

3. 验证 Origin/Referer 头

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:

# 推荐: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完全指南,关键安全要点:

要点 说明
算法指定 验证时必须指定 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 凭证)

防御

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 集成
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 配置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# FastAPI 中间件
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(HTTPSRedirectMiddleware)

禁用弱密码套件(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 DENYSAMEORIGIN 防止点击劫持
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. 最小化信任:即使是内部服务之间的请求也要验证身份
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()

密钥与凭证管理

# 错误:硬编码密钥
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 auditsafety 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。

# 错误:字符串拼接,存在注入风险
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,绕过认证。

原因: 使用简短密钥(如 secret123456)容易被字典攻击;硬编码在源码中一旦仓库泄露立即失效。

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


参见

阅读更多

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog

数据结构完全参考

Python list 是动态数组(Dynamic Array): 推论:需要频繁头部操作时,用 collections.deque 替代 list。 循环链表将尾节点的 next 指向头节点。常见于约瑟夫环问题。使用时注意遍历终止条件应以初始节点为准,否则会无限循环。 栈遵循 LIFO(后进先出)原则。 队列遵循 FIFO(先进先出)原则。 Python heapq 是最小堆。实现最大堆时将值取反。 单调栈维护一个单调递增或递减的栈,用于求"下一个更大/更小元素"。 单调队列用于滑动窗口最值问题,队列中元素单调递减(求最大值)或单调递增(求最小值)。

By yellowdog