SQLAlchemy 2.0 的 text() 不防注入,必须用命名参数+字典传参;动态标识符(表名/字段名/排序)须白名单校验;IN 列表用 tuple 绑定;禁止字符串拼接和 literal_column。

SQLAlchemy 2.0 的 text() 不自动防注入,安全完全取决于你是否用命名占位符 + 字典传参,且字段名/表名/排序方向等动态标识符必须白名单校验。
text() 必须配命名参数,不能拼字符串
很多人以为调用了 text() 就进了“安全区”,其实它只是个 SQL 字符串包装器,不做任何解析或转义。一旦你用 f"..." 或 "..." + user_input 拼接,就等于把攻击面直接暴露给数据库。
- ❌ 错误:
text(f"SELECT * FROM users WHERE name = '{name}'")—— 单引号闭合后可执行任意语句 - ❌ 错误:
text("SELECT * FROM users WHERE id IN (" + ",".join(ids) + ")")——ids若含恶意内容会直接执行 - ✅ 正确:
text("SELECT * FROM users WHERE name = :name AND status = :status"),再用execute(stmt, {"name": name, "status": status}) - ⚠️ 注意:
:name中的冒号不能省;?name或%s在text()中无效;元组传参如(name, status)易错位,不推荐
WHERE 中的 IN 列表要用 tuple + 命名绑定
IN 子句是高频翻车点。SQLAlchemy 不支持直接展开列表,但也不能手动拼接字符串——正确做法是用 tuple() 转换输入,并在 SQL 中用命名占位符接收。
- ✅ 安全写法:
stmt = text("SELECT * FROM orders WHERE id IN :order_ids"),然后execute(stmt, {"order_ids": tuple(user_ids)}) - ⚠️ 关键:
user_ids必须是 Python list/tuple,且元素为合法值(如 int/str);空列表需单独处理(tuple([])会报错,应提前拦截) - ❌ 不要写
"IN (" + ",".join(map(str, ids)) + ")",哪怕你过滤了数字也不够——类型绕过、负数、科学计数法都可能触发异常或注入
ORDER BY / GROUP BY / 表名 / 字段名无法参数化,必须白名单
SQL 协议本身不允许把标识符(identifier)当参数传,所以 :sort_field 这种写法在 text() 里会直接报错或被当作文本字面量。这些地方的安全靠的是严格校验,不是参数绑定。
- ✅ 排序字段白名单:
allowed_sorts = {"created_at": "created_at", "username": "username", "id": "id"},然后sort_sql = allowed_sorts.get(request.args.get("sort"), "id") - ✅ 动态表名校验:
if table_name not in ["users", "posts", "comments"]: raise ValueError("Invalid table") - ✅ 字段名校验(ORM 场景):
if field not in User.__table__.columns.keys(): raise ValueError("Unknown column") - ⚠️
literal_column(request.args.get("col"))是高危操作,等同于放行字符串进 SQL 结构,禁用
filter() 和 filter_by() 安全,但别混进字符串逻辑
ORM 查询方法默认走表达式树编译,天然参数化,但前提是别在中间掺杂字符串操作。常见误区是“半 ORM 半原生”写法,看似安全实则已破防。
- ✅ 安全:
query.filter(User.email == email_input)、query.filter_by(status="active") - ✅ 安全(模糊匹配):
query.filter(User.name.like(f"%{q}%"))—— 注意:这里f"%{q}%"是 Python 字符串格式化,只要q不含%或_就没问题;若需用户控制通配符,改用like(q, escape="\")并校验 escape 字符 - ❌ 危险:
query.filter(text("name = '" + q + "'"))—— 绕过所有 ORM 防护 - ❌ 危险:
query.filter(User.name.like("%" + q + "%"))—— 如果q是"test%'; DROP TABLE users--",f-string 已在 Python 层完成拼接,SQLAlchemy 来不及干预
最易被忽略的一点:动态 SQL 的“结构可控性”比“数据可控性”更难保障。参数化只管值,不管语法;而字段名、函数名、JOIN 方式、甚至是否加 LIMIT,都得靠代码逻辑兜底。白名单不是可选项,是强制项。

















