使用 text() 是执行原生 SQL 的唯一安全方式,必须用 :name 占位符和字典参数绑定值;表名、列名、排序字段需白名单校验;模糊搜索应限制输入长度并用 ilike(),所有动态字段须服务端重复校验。

直接用 db.session.execute() 执行字符串拼接的 SQL,哪怕只有一处,就等于把数据库钥匙交到前端手里。
text() 是唯一能执行复杂 SQL 的安全入口
SQLAlchemy 不允许你传原始字符串进 execute(),必须包装成 text() 对象。这不是形式主义,而是强制你显式声明“我要脱离 ORM 安全层”,从而触发参数绑定检查。
-
db.session.execute("SELECT * FROM users WHERE id = %s", (1,))❌ 会抛TypeError:驱动不认这种裸字符串 -
db.session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": 1})✅ 唯一被接受的原生查询写法 - 占位符必须是
:name形式,%s或?name在text()中完全无效 - 第二个参数必须是字典,传元组会错位,传
**locals()可能键名不匹配
ORDER BY / 表名 / 列名必须白名单校验
数据库协议本身禁止把表名、列名、排序字段当参数传——这不是 SQLAlchemy 的锅,是 SQL 引擎解析阶段就定死的规则。任何试图用 :sort_field 绑定列名的操作,都会在运行时报错或静默失效。
创建 Flask + Vue.js 全栈项目,采用单端口架构、uv 依赖管理、结构化日志和生产级前端设计。当用户要求以下操作时触发:创建新的 Flask+Vue 项目、搭建 Web 应用、构建全栈 Python+Vue 应用、初始化 Flask Web 项目、设置单端口 Flask 前后端项目。触发词:新建项目、创建项目、搭建项目、Flask项目、Vue项目。
- 翻车典型:
text("SELECT * FROM users ORDER BY :sort").bindparams(sort=sort_param)❌ 实际生成的是ORDER BY 'username'(带引号,变成字符串字面量) - 正确做法:先校验再拼接:
if sort_field not in ["id", "created_at", "username"]: raise ValueError("Invalid sort field") - 动态列查询要用
getattr(User, field),但前提是field in User.__table__.columns.keys() - 别信
replace("'", "''")或正则删--,Unicode 编码和十六进制绕过太容易
模糊搜索别用 f"" 拼 like,用 ORM 的 like() 方法
User.name.like(f"%{q}%") 看似无害,实则已在 Python 层完成字符串拼接,ORM 来不及拦截。真正安全的 like() 是构造表达式树,最终生成带占位符的预编译语句。
-
filter(User.name.like(f"%{q}%"))❌ 危险:f"" 提前展开,q被直接塞进 SQL 字符串 -
filter(User.name.like("%" + q + "%"))❌ 同样危险:+ 拼接无法被 ORM 参数化 -
filter(User.name.like(f"%{q}%"))仅当q是硬编码常量才安全(比如f"%{DEFAULT_PREFIX}%") - 需要动态模糊搜索时,坚持用
filter(User.name.ilike(f"%{q}%"))并确保q已经过长度/字符集限制(如q[:50])
最易被忽略的一点:所有 text() 查询都必须配字典参数,哪怕只有一个变量;而白名单校验不能只做一次——前端可能绕过 JS 校验、伪造 raw body、删掉 Content-Type,所以服务端每处动态字段都要独立校验。

















