sqlalchemy 的 filter() 和 filter_by() 默认安全,因其构建表达式树并生成参数化查询;但 text()、字符串拼接等会绕过防护导致 sql 注入风险。

SQLAlchemy 的 filter() 和 filter_by() 方法本身不执行字符串拼接,而是构建表达式树,最终由底层驱动转为参数化查询——这是它防 SQL 注入的底层机制。但这个防护只在「你没主动破坏它」的前提下生效。
filter() 和 filter_by() 为什么默认安全
它们不是把 Python 字符串塞进 SQL,而是把 User.name == name 这样的比较操作编译成数据库无关的表达式对象,再交由方言(dialect)生成带占位符的语句,例如:SELECT * FROM users WHERE name = ?,然后把 name 值作为独立参数传给驱动。
-
filter_by(username="admin")只支持等值匹配,且字段名必须是模型属性名(硬编码),天然规避动态字段风险 -
filter(User.status == 1, User.created_at > now)支持复杂条件,但所有值都走参数绑定,包括数字、字符串、日期甚至None - 哪怕传入的是用户输入的
request.args.get("q"),只要没用format、f-string或+拼进 SQL 字符串,就仍是安全的
text() 是安全边界断裂点,不是“高级用法”
一旦你写 text("SELECT * FROM users WHERE name LIKE '%{q}%'") 或 text(f"ORDER BY {sort_field}"),SQLAlchemy 就彻底放弃防护——它只负责把那段字符串原样发给数据库,不校验、不转义、不参数化。
- 正确做法是:用
text("... LIKE :pattern")+.bindparams(pattern=f"%{q}%") - 排序字段等无法参数化的部分,必须白名单控制:
if sort_field not in ["created_at", "username"]: raise ValueError("Invalid sort field") -
text()应该只出现在固定结构 SQL 中,比如窗口函数或 CTE,且所有变量必须通过bindparam()注入
db.session.execute() 的两种调用方式风险天差地别
新手常以为用了 Flask-SQLAlchemy 就自动免疫,结果在 db.session.execute("UPDATE users SET status = 'active' WHERE id = {}".format(user_id)) 里中招——这行代码根本没触发 ORM 的任何安全逻辑,纯字符串拼接。
- 安全写法一(ORM 对象更新):
user = User.query.get(user_id); user.status = "active"; db.session.commit() - 安全写法二(原生 SQL + 参数):
db.session.execute(text("UPDATE users SET status = :status WHERE id = :id"), {"status": "active", "id": user_id}) - 绝对避免:
db.session.execute(sql_string.format(...))、db.session.execute(f"...")、db.session.execute("..." % ...)
like()、in_()、ilike() 这些方法才是模糊/批量查询的正确入口
很多人为了“灵活”自己拼 LIKE 条件,比如 filter(text("name LIKE '%{}%'".format(keyword))),这等于绕过 ORM 直接开后门。
- 用
User.name.like(f"%{keyword}%"),生成的是WHERE name LIKE ?+ 参数,安全 - 批量 ID 查询用
User.id.in_([1, 2, 3]),不是text("id IN ({})".format(",".join(ids))) - 大小写不敏感搜索用
ilike(),不是手写LOWER(name) = LOWER('{kw}')
真正容易被忽略的点在于:ORM 的防护是「被动生效」的——它不会阻止你写错,也不会报错提醒你正在退出安全区。只要你碰了 text()、format()、f-string 或裸字符串拼接,那道防线就瞬间瓦解。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











