SQLAlchemy 2.0防注入机制未变,仍依赖参数化查询和表达式树构建;但API全面转向显式参数绑定,混用字符串拼接或位置参数将失效,filter()等ORM方法安全的前提是不嵌入f-string或手动拼接SQL。

SQLAlchemy 2.0 的防注入机制没变,但 API 调用方式变了——不升级写法,就等于裸奔。session.execute()、text()、select() 等核心接口全部转向显式参数绑定和表达式树构建,混用旧式字符串拼接或位置参数会直接失效。
filter() 和 filter_by() 仍安全,但别在里头塞 f-string
ORM 查询方法本身没风险,前提是别主动破坏它。只要值是独立传入的,SQLAlchemy 就会走参数化;一旦你提前把用户输入拼进字符串里,防护就彻底绕过。
-
query.filter(User.name == name_input)✅ 安全:生成WHERE name = %s+ 单独参数 -
query.filter_by(email=email_input)✅ 安全:仅支持等值,自动绑定 -
query.filter(f"name LIKE '%{q}%'")❌ 危险:Python 层已拼好恶意字符串,数据库收到的就是完整攻击载荷 -
query.filter(text("name = '" + q + "'"))❌ 同样危险:text()不解析内容,只当原始 SQL 发送
text() 必须配命名参数字典,:param 冒号不能少
text() 在 2.0 中仍是原生 SQL 入口,但它只是个容器——真正起作用的是你传进去的 params。驱动只认 :name 这种命名占位符,? 或%s 会被当普通字符处理,甚至报错。
-
session.execute(text("SELECT * FROM users WHERE status = :status"), {"status": "active"})✅ 驱动预编译,值被隔离 -
session.execute(text("SELECT * FROM users WHERE id = :id"), (123,))❌ 元组传参错位,键名不匹配直接抛KeyError -
session.execute(text(f"SELECT * FROM logs WHERE ip = '{ip}'"))❌ 字符串插值,ip里的' OR 1=1 --直接进 SQL -
session.execute(text("ORDER BY :field"), {"field": "name"})❌ 字段名无法参数化,语法错误,不是安全问题而是 SQL 解析失败
表名、字段名、ORDER BY 这些必须白名单硬控
SQL 协议本身不允许参数化结构信息。FROM ?、ORDER BY ? 是非法语法,不是 SQLAlchemy 拦不住,是数据库压根不接受。所有动态元信息都得在 Python 层做校验,不能依赖“传参”幻想。
立即学习“Python免费学习笔记(深入)”;
- 排序字段:
allowed = {"id", "created_at", "email"}; sort_col = request.args.get("sort", "id"); if sort_col not in allowed: sort_col = "id" - 表映射:
table_map = {"user": User, "post": Post}; model = table_map.get(request.args.get("type")),拒绝globals()[user_table]类反射 - 动态列查询:
if field_name not in User.__table__.columns.keys(): raise ValueError("Invalid column"),再用getattr(User, field_name) -
literal_column(request.args.get("col"))❌ 主动放弃防护,等于邀请注入
Core 查询比手写 text() 更可靠,但 bindparam() 别乱套
用 select() + 表对象构造查询,SQL 结构由 Python 对象定义,用户输入只进 bindparam(),天然隔离代码与数据。但要注意:在 text() 里混用 bindparam() 没意义,它不改字符串内容,只影响后续绑定逻辑。
-
stmt = select(User).where(User.email == bindparam("email")); session.execute(stmt, {"email": user_input})✅ 推荐:结构固定,值可变 -
text("SELECT * FROM users WHERE email = :email").bindparam("email", user_input)❌ 无效:bindparam() 不是链式调用方法,text()返回的是字符串包装器,无此属性 -
select().where(text("email = '" + user_input + "'"))❌ 错上加错:外层 select 不拯救内层字符串拼接
真正容易被忽略的,是那些“看起来像参数化”的假安全点:filter(text("..."))、extra(order_by=[f"..."])、getattr(model, user_field) —— 它们全都不触发参数化,全靠你手动校验。SQLAlchemy 2.0 没给你加新锁,只是把旧钥匙换了个更严格的握法。


















