Python Flask 因其轻量和灵活被大量用于快速构建 Web 应用,但默认的 Jinja2 模板引擎和数据库抽象层并不能完全阻止开发者写出有漏洞的代码。SQL 注入和 XSS 依然是 OWASP Top 10 中的常客。要解决这两个问题,不能只靠框架本身,必须引入专门的插件和库,并在架构层面建立纵深防御。下面直接拆解具体工具和落地方法。

SQL 注入防护的核心插件与库

Flask 本身不强制要求使用 ORM,但如果你还在用原生 SQL 拼接字符串,任何插件都救不了你。SQL 注入的根因是数据和指令的混淆,所以第一层防护就是参数化查询。即便不用重型 ORM,也要用底层库提供的参数绑定能力。

SQLAlchemy 是 Flask 生态的事实标准,它的 ORM 层和 Core 层都强制使用参数绑定。当你在 Flask-SQLAlchemy 中这样写代码时,底层自动转义:

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# 安全的 ORM 查询,自动参数化
user = db.session.query(User).filter(User.username == input_username).first()

即使使用原生 SQL,SQLAlchemy 的 text() 构造也支持绑定参数,务必避免字符串格式化:

from sqlalchemy import text
# 正确做法:使用命名参数
result = db.session.execute(text("SELECT * FROM users WHERE email = :email"), {"email": user_input})

如果你的项目不需要完整的 ORM,或者你在处理极其复杂的动态查询,可以单独引入 pymysql 或 psycopg2,但必须使用它们提供的参数化游标。更轻量的选择是使用 PonyORM 或 Peewee,它们同样强制参数化,且学习成本更低。Peewee 的 Flask 集成插件 peewee-flask 能让模型定义变得直观,同时杜绝注入。

对于需要动态构建查询条件的管理后台,比如用户可以根据多个字段组合筛选,直接拼接 WHERE 子句是危险的。此时应使用 SQLAlchemy 的查询构造器或类似 sqlalchemy-filters 这样的库,它允许前端传递安全的过滤 JSON,在后端转换成 ORM 过滤条件,完全避免拼接 SQL 字符串。另一个强大的工具是 Flask-MongoEngine,如果你用 MongoDB,它也通过文档查询语法避免了注入,但需注意正则表达式注入风险,应使用 mongoengine.queryset.visitor 进行严格校验。

XSS 防护的模板层与净化库

Flask 的 Jinja2 模板引擎默认开启自动转义,对 {{ }} 输出的内容会转义 HTML 实体。这意味着在大部分情况下,你什么都不做就能防御反射型 XSS。但很多开发者会在需要渲染富文本时,使用 |safe 过滤器或 Markup() 函数,这就打开了缺口。必须明确:永远不要对用户输入的内容使用 |safe,除非你经过了严格的净化。

对于富文本输入,业界标准方案是 Bleach,它基于白名单机制,只允许指定的标签和属性通过。虽然 Bleach 已进入维护模式,但它的替代品 nh3 更快且更安全,直接绑定 Rust 的 ammonia 库。在 Flask 中,你可以封装一个自定义过滤器:

import nh3
from flask import Markup

def sanitize_html(text):
    allowed_tags = {'p', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'br'}
    allowed_attributes = {'a': ['href', 'title']}
    clean = nh3.clean(text, tags=allowed_tags, attributes=allowed_attributes)
    return Markup(clean)

# 注册为模板过滤器
app.jinja_env.filters['sanitize'] = sanitize_html

在模板中,对需要渲染富文本的变量使用该过滤器:{{ user_content|sanitize }}。这能确保只输出安全的 HTML 结构。另一个选择是 bleach 的维护分支 bleach-allowlist,但 nh3 在性能和内存安全上更优。

对于 JSON 响应中的 XSS 风险,不要手动拼接 JSON 字符串。始终使用 flask.jsonify(),它会设置正确的 Content-Type 并转义特殊字符。如果前端将 JSON 数据插入 DOM,必须在前端使用 textContent 而非 innerHTML,或使用 DOMPurify 这类前端库。后端可以提供内容安全策略(CSP)头来作为纵深防御,Flask-Talisman 插件可以一键配置 CSP、HSTS 等安全头:

from flask_talisman import Talisman
Talisman(app, content_security_policy={
    'default-src': "'self'",
    'script-src': "'self' 'nonce-{random}'",
    'style-src': "'self' 'unsafe-inline'"
})

配置 CSP 后,即使攻击者注入了脚本标签,浏览器也不会执行。注意,使用 nonce 或 hash 模式时,需要配合模板动态生成 nonce 值,Flask-Talisman 支持通过回调函数实现。

输入校验与类型安全层

很多注入和 XSS 的根源是输入校验不严。Flask 没有内置的请求数据校验机制,但可以集成 Marshmallow 或 Pydantic。Marshmallow 配合 webargs 能对请求参数进行序列化、反序列化和严格类型校验,任何不符合 schema 的数据都会被拒绝,从源头截断攻击载荷:

from marshmallow import Schema, fields, validate
from webargs.flaskparser import use_args

class UserQuerySchema(Schema):
    username = fields.Str(required=True, validate=validate.Length(min=1, max=50))
    age = fields.Int(validate=validate.Range(min=0, max=150))

@app.route('/search')
@use_args(UserQuerySchema(), location="query")
def search(args):
    # args 已经是经过校验和清洗的字典
    user = db.session.query(User).filter_by(username=args['username']).first()
    return jsonify(user)

Pydantic 是另一个高性能选择,通过 Flask-Pydantic 插件,可以直接在路由装饰器上声明校验模型。它利用 Python 类型提示,在运行时强制转换和校验,任何类型错误或越界数据都会立即返回 422 错误,不会进入业务逻辑。

对于路径参数和表单数据,同样需要严格的白名单校验。例如,如果某个参数只应该是整数,就用 int() 转换并捕获 ValueError,而不是直接传给数据库。Werkzeug 提供的路由转换器也可以做初步的类型约束。

ORM 之外的查询安全:动态排序与字段名

一个常被忽视的注入场景是动态排序或分组字段。很多应用允许用户点击表头排序,后端直接接收字段名拼接到 SQL 的 ORDER BY 或 GROUP BY 中。参数化查询通常无法绑定列名或表名,这时必须使用白名单映射。你可以维护一个允许排序的字段集合,将用户输入映射到实际列名:

ALLOWED_SORT_COLUMNS = {'id', 'username', 'email', 'create_time'}
sort_param = request.args.get('sort', 'id')
if sort_param not in ALLOWED_SORT_COLUMNS:
    abort(400)
# 安全地使用白名单中的列名
query = query.order_by(getattr(User, sort_param))

这种方式完全避免了字符串拼接,利用 ORM 的属性访问来构建查询。如果必须使用原生 SQL,也要用同样的白名单逻辑验证列名,并使用 SQLAlchemy 的 Identifier 准备机制或直接使用 text() 的绑定参数(仅限值,列名仍需白名单)。

纵深防御:安全头与日志监控

除了上述专项防护,还需要部署通用的安全层。Flask-Talisman 除了 CSP,还能强制 HTTPS、设置 X-Content-Type-Options、X-Frame-Options 等头。对于登录和敏感操作,Flask-Login 和 Flask-Principal 管理会话和权限,但要注意会话固定和 cookie 安全,设置 session cookie 的 httponly、secure 和 samesite 属性。

日志和监控也是防护的一环。集成 Flask-Logging 或 Sentry 的 Flask 集成,可以实时捕获异常和可疑输入。当检测到连续的 SQL 异常或包含 script 标签的请求时,应触发告警。可以编写一个简单的请求拦截中间件,用正则匹配常见的 XSS 和 SQL 注入探测载荷,记录 IP 并临时封禁,这能抵御自动化扫描工具。

最后,定期使用 Bandit 扫描代码中的安全漏洞,用 Safety 检查依赖库的已知漏洞,这些工具可以集成到 CI 流程中。对于 Flask 应用,Bandit 能检测出使用 mark_safe()、直接拼接 SQL 字符串等高危模式。

工具选型总结:SQL 注入防护首选 SQLAlchemy + Flask-SQLAlchemy,轻量场景用 Peewee;动态查询用 sqlalchemy-filters;XSS 净化用 nh3 替代 Bleach;全局安全头用 Flask-Talisman;输入校验用 Marshmallow/webargs 或 Pydantic;动态列名用白名单映射。这些插件组合起来,可以覆盖绝大多数攻击面,而且不拖累开发效率。