如何修复 SQLAlchemy 原生 SQL 注入风险

小满
小满
发布时间:2026-08-25| 23人浏览过
原创

前言

在 SQLAlchemy 中写原生 SQL,最容易出问题的并不是语法本身,而是把 `text()` 误当成自动防注入的保护层。一旦把用户输入直接拼进查询,或把动态列名、`IN` 条件处理得过于随意,ORM 也兜不住。本文按四类高频场景拆解安全写法,帮助你快速判断哪些代码只是“能跑”,哪些才是真的可信。

很多项目在需要复杂查询、报表接口或历史代码兼容时,还是会回到 SQLAlchemy 的原生 SQL 写法。但一旦把 text() 误当成“自动防注入”的保险层,风险往往就从这里开始。下面按最常见的四种场景拆开说明:哪些写法会直接失守,哪些边界必须靠参数绑定或白名单来兜住,以及如何判断自己的查询到底是不是真的安全。

text() 不是防火墙,拼字符串才是风险起点

不少人看到 text(),就默认自己已经进入了“ORM 安全区”。但实际上,text() 只负责承载 SQL 文本,并不会检查你是不是把用户输入直接塞进了字符串。

参数绑定与字符串拼接风险对比手绘图
参数绑定和拼接风险对照用左右对照方式展示 `text()` 的安全边界:左侧是命名占位符加参数字典,右侧是。

例如下面这种写法,本质上已经把注入入口写进了 SQL:

text(f"SELECT * FROM users WHERE name = '{name}'")

SQLAlchemy 不会扫描 f-string 里的变量,它真正识别的是占位符,比如 :name 或 ?。如果不是占位符,而是先在 Python 里完成字符串拼接,那就等于绕过了参数绑定。

这类错误通常会直接体现在结果上:

  • 当用户传入 name="admin' OR 1=1 --",查询可能返回全部用户。
  • 更糟的情况下,如果传入 "admin'; DROP TABLE users; --",在支持多语句执行的驱动配置下,甚至可能触发删表。

安全写法应当把 SQL 和参数分开:

text("SELECT * FROM users WHERE name = :name")
session.execute(..., {"name": name})

几种常见危险写法也很典型:

  • text("SELECT * FROM users WHERE name = '" + name + "'")
  • text(f"SELECT ... WHERE name = '{name}'")
  • text("SELECT ... WHERE name = %s" % name)

这里最容易被忽略的一点是:text() 本身不会自动触发参数绑定,必须和 execute() 的第二个参数配合使用,通常是 dict 或 tuple。只有这样,数据库驱动才会把用户输入当作参数值,而不是 SQL 语法的一部分。

ORDER BY、GROUP BY 这类动态标识符,不能靠 :param 解决

另一个高频误区是把列名、表名这类标识符也当成普通参数处理,比如写成:

动态排序白名单校验手绘图
动态字段名的安全边界把可选字段、方向校验和最终拼接拆成三步,说明为什么列名不能参数化,以及白名单为什么比正则更可靠。
ORDER BY :sort_col

这并不会把 :sort_col 解析成列名,而更可能变成类似 ORDER BY 'email' 的形式。结果要么是无效 SQL,要么数据库把它当作常量处理,排序逻辑直接失效。

这不是 SQLAlchemy 的特殊问题,而是 SQL 标准本身的限制:标识符,也就是表名、列名、函数名,无法像值那样参数化。这里唯一可靠的办法,就是白名单。

比较稳妥的做法是把允许使用的字段提前写死:

allowed_cols = {
    "email": "users.email",
    "created_at": "users.created_at",
    "status": "users.status"
}

然后在进入 SQL 拼接前先校验:

if sort_by not in allowed_cols:
    raise ValueError("Invalid sort field")

确认值在白名单里之后,再拼接到 SQL 中:

f"ORDER BY {allowed_cols[sort_by]} {direction}"

但这里还没结束,direction 同样不能直接信任,也必须限制在明确范围内,例如只允许 asc 和 desc。

很多人会尝试用正则过滤,比如 /^[a-z_]+$/,看起来能挡掉特殊字符,实际上并不可靠。因为风险点不只在字符是否合法,还在于 SQL 语义是否被扩大。像 email ASC, (SELECT password FROM admins) 这样的输入,靠简单正则并不能建立真正可信的边界。

这类场景里,真正关键的往往不是 SQL 语句本身,而是那句看起来很普通的白名单判断。它虽然不在数据库层,却决定了整条查询是不是可信。

session.execute() 和 connection.execute() 怎么传参,差别在哪里

session.execute() 和 connection.execute() 都能配合 text() 与参数字典使用,但它们背后的事务管理和连接生命周期不同,踩坑时表现也不一样。

安全的基本形式是一致的:

session.execute(text("... :x"), {"x": val})
engine.connect().execute(text("... :x"), {"x": val})

前者更适合放在 ORM 会话管理里,后者也能安全执行,但需要你自己处理连接关闭,比如手动 close()。

实际项目里更危险的问题,通常不是 API 选型,而是参数风格混用。比如这样写:

connection.execute("... %s", [val])

它在不同数据库驱动下的行为并不统一:

  • PostgreSQL 的 psycopg2 不认这套 SQLAlchemy 文本风格;
  • SQLite 只认 ?;
  • MySQL 某些场景才使用 %s。

一旦项目里把这些占位符风格混着写,就很容易出现报错、参数失效,或者开发者为了“先跑通”退回字符串拼接,最终把安全边界重新拆掉。

如果想降低团队维护成本,比较现实的统一方案就是:全项目固定使用命名占位符 :x,并统一通过 dict 传参。这样兼容性更稳定,可读性也更高,出了问题更容易排查。

IN 查询不要手搓占位符,交给 SQLAlchemy 处理更稳妥

报表、批量筛选、后台列表接口里,经常会出现 WHERE id IN (...) 这样的条件。很多问题就出在这里:开发者手动拼接占位符数量,结果一旦参数个数变化,SQL 就开始变脆。

IN 查询处理方式对比手绘图
IN 查询的稳妥写法用卡片对比表达式写法、原生 SQL 元组传参、空列表校验和手工占位符拼接的常见问题。

如果你使用的是表达式方式,推荐直接交给 SQLAlchemy 处理:

select(User).where(User.id.in_(user_ids))

这里的 user_ids 是 Python list,SQLAlchemy 会自动把它展开成参数化的 IN 表达式。这种方式通常比手工拼接更稳,也更容易维护。

如果必须写原生 SQL,也可以这样写:

text("SELECT * FROM users WHERE id IN :ids")
{"ids": tuple(user_ids)}

这里有个细节不能忽略:部分驱动不接受 list,要传 tuple 才能正常处理。

相反,下面这种做法风险很高:

f"IN ({', '.join(['%s'] * len(ids))})"
execute(sql, ids)

问题至少有两个:

  • 如果 ids 是空列表,拼出来的 SQL 很容易直接语法错误。
  • 如果参数类型控制不严,仍然可能把不该进入查询的内容带进去。

因此,除了选择正确的展开方式,前置校验也不能省:

if not isinstance(user_ids, list) or not user_ids:

像空数组、非数组输入,都应该在进入查询前直接拒绝。因为 IN 查询的问题往往不只是“怎么拼”,而是你是否明确控制了输入结构。

真正决定查询是否可信的,往往是 SQL 外面的那层约束

修复 SQLAlchemy 原生 SQL 注入风险,核心并不复杂:值用参数绑定,标识符用白名单,批量条件交给框架展开,传参风格保持统一。

真正难的是纪律性。很多漏洞不是因为不会写 text("... :x"),而是项目里总有人在动态排序、批量查询、兼容旧驱动这些边界场景中图省事,重新退回拼字符串。只要还存在这类写法,ORM 再完整,也替你挡不住注入。

可以把检查重点压缩成三件事:第一,看 SQL 中是否仍有用户输入直接参与字符串拼接;第二,看列名、排序方向这类内容是否经过白名单;第三,看 execute() 是否真正收到了参数字典而不是被动接受拼好的 SQL。把这三层守住,原生 SQL 依然可以用得灵活,而且不至于把安全边界主动交出去。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。