SQLAlchemy的query()和filter()默认通过参数化绑定防SQL注入,但动态字段名、表名等元信息需白名单校验;text()必须用:placeholder+字典传参,禁用字符串拼接。

SQLAlchemy的query()和filter()默认不防SQL注入?
不是“默认防”,而是——只要不用字符串拼接,就天然免疫。很多人误以为filter()里写name == request.args.get('q')就安全,其实它只是把值当Python对象传给SQLAlchemy,底层走参数化绑定(如WHERE name = %s),不会拼进SQL字符串。但一旦你用f"name LIKE '%{q}%'"或text("SELECT * FROM user WHERE name = '" + q + "'"),防线立刻崩溃。
常见错误现象:sqlalchemy.exc.ProgrammingError: (psycopg2.errors.SyntaxError) syntax error at or near "OR",说明用户输入的' OR 1=1 --被原样塞进了SQL里。
- 永远别用
+、f""、.format()拼接SQL片段 -
text()必须配合bindparam或字典传参,不能直接插变量 - 像
order_by(request.args.get('sort'))这种,字段名本身不能参数化,得白名单校验
哪些地方必须手动白名单校验?
SQLAlchemy能帮你绑参数,但没法判断“request.args.get('order')是不是合法字段名”或“request.json.get('table')是不是真实表名”。这类元信息(field/table/operation)在SQL生成前就得拦住。
使用场景:动态排序、多表联合、条件字段可选(比如前端传filter_by=age或filter_by=status)。
- 排序字段:只允许
['id', 'created_at', 'name'],其他一律拒掉 - 查询字段:用
getattr(User, field)前先检查field in User.__table__.columns.keys() - 表名/模型名:映射到固定字典,比如
{'user': User, 'post': Post},而不是globals()[table_name]
execute(text())怎么写才安全?
绕过ORM直连SQL时,最容易翻车。关键不是“能不能用text()”,而是“参数怎么进”。直接str.replace()或f""等于裸奔。
性能影响:用bindparam或字典传参,和ORM普通查询一样走预编译,没额外开销;手拼则每次都要重新解析SQL。
- ✅ 安全写法:
db.session.execute(text("SELECT * FROM user WHERE status = :status"), {'status': 'active'}) - ❌ 危险写法:
db.session.execute(text(f"SELECT * FROM user WHERE status = '{status}'")) - ⚠️ 注意:
:status里的冒号不能少,PostgreSQL/MySQL驱动都依赖这个语法识别占位符
Flask-SQLAlchemy里raw_query不存在,别信网上乱抄的代码
很多搜索结果教你加个raw_query方法,或者 monkey patch db.session,其实没必要,也容易引入兼容性问题。Flask-SQLAlchemy 3.x 已弃用session.execute()的旧签名,新写法统一用text() + 参数字典。
容易踩的坑:db.session.execute("SELECT * FROM user").fetchall()在新版会报TypeError: execute() missing 1 required positional argument: 'statement',因为字符串不再自动转text()。
- 必须显式包一层
text(),哪怕最简单的查询 - 如果用了
bindparam('x', value),记得在text()里写:x,名字要一致 - SQLite对命名参数支持弱,建议统一用
?位置参数(text("SELECT ?")+[value])避免冷门报错
真正的难点从来不在“怎么写安全SQL”,而在于分清哪里是数据、哪里是结构——前者交给SQLAlchemy,后者得你自己守门。漏掉一个字段名校验,前面所有参数化都白搭。










