二次sql注入在flask+sqlalchemy中并非默认风险,因其仅发生在数据库返回值被未经校验拼入新sql时;orm的filter()、filter_by()、get()等方法均强制参数化,只要不手动f-string拼接(如f"update...{nickname}")或误用text()未绑定字典参数,即可避免。

二次SQL注入在Flask+SQLAlchemy中几乎不会发生——只要你没把数据库返回值再拼进新SQL里;但很多人误以为“查出来再拼一次”是安全的,结果栽在这里。
为什么二次SQL注入在ORM里不是默认风险?
SQLAlchemy 的 filter()、filter_by()、get() 等方法,底层全部走参数化查询。哪怕你查出一个字符串值(比如 user.nickname),只要后续不把它用 f"" 或 + 拼进新 SQL,就不会触发二次注入。
- ✅ 安全:查出
nickname = db.session.query(User.nickname).filter(User.id == 123).scalar(),然后直接用于日志或前端展示 - ❌ 危险:查出
nickname后又写db.session.execute(f"UPDATE logs SET msg = '{nickname}' WHERE id = 456") - ⚠️ 特别注意:
text()不会自动识别变量来源——它只认你传进去的字典,不管这值是用户输入还是数据库查出来的
哪些场景容易诱发二次注入?
真正危险的是“先查后拼”的链路,尤其是涉及动态构造条件、日志埋点、审计记录、缓存键生成等环节。
- 从数据库读取排序字段名(如
sort_col),再用于ORDER BY:必须白名单校验,不能因为“这值来自 DB 就放行” - 读取用户自定义 SQL 片段(如保存在配置表里的
WHERE条件),再用text()执行:等同于执行不可信代码,应彻底禁止 - 把用户可控字段(如
email)查出来后,又拼进另一个LIKE查询:User.name.like(f"%{db_fetched_email}%")—— 这里%和通配符本身可能被滥用 - 日志系统把数据库字段值拼进 SQL 插入语句(例如审计日志写库):应统一走
session.execute(text(...), {...}),而非字符串格式化
怎么写才真能防住二次注入?
核心原则只有一条:**任何进入 SQL 结构或值位置的数据,无论来源,都必须经过同一套校验/参数化逻辑。** 数据库返回值 ≠ 可信输入。
- 所有
text()查询,第二个参数必须是显式构造的字典,且键名与占位符严格一致:{"col": safe_value},禁用**locals()或元组 - 动态列名、表名、
ORDER BY字段,一律白名单硬编码:if sort_field not in ["created_at", "score"]: abort(400) - 模糊搜索统一走 ORM 方法:
User.email.like("%" + keyword + "%")(注意:keyword 仍需提前过滤%、_等通配符,或改用ilike()+ 参数化) - 避免“查-改”两步走:想更新某字段为另一字段值,优先用原生 SQL 的
SET col1 = col2,而不是先SELECT col2再UPDATE ... SET col1 = f"{fetched}"
最容易被忽略的细节
很多人卡在“我用了 ORM,也用了 text() + 字典”,却还是出问题——原因往往是:占位符写成 ?name 或 %s,而 SQLAlchemy 的 text() 只认 :name 风格;或者字典里键名拼错,导致参数根本没绑定上,SQL 里还留着裸字符串。
更隐蔽的是:把 JSON 字段解析后的内容(如 user.settings.get("default_sort"))直接当排序字段用——这个值虽来自 DB,但源头是用户提交的,没过白名单就等于开了后门。











