ORM原生接口如raw()、text()、execute()绕过参数化保护,表名、字段名等结构部分无法参数化,必须通过白名单校验用户输入。

ORM框架的raw()、execute()、text()等原生SQL接口会绕过参数化保护
ORM本身不等于自动免疫SQL注入。像 Django 的 raw()、extra(),SQLAlchemy 的 text() 和 execute(),或者 Laravel 的 DB::select(DB::raw(...)),这些方法允许你直接写 SQL 字符串——一旦拼接用户输入,就回到“字符串拼接”老路。
常见错误场景:
- 用
f"SELECT * FROM users WHERE name = '{name}'"拼接后传给text() - 在
raw()中硬编码用户传入的order_by字段名(如ORDER BY {user_input}) - 把未经验证的 URL 参数直接塞进
extra(where=[...])的 where 条件里
动态表名、列名、排序字段无法用参数占位符绑定
SQL 标准规定:预编译参数只能替代「值」(value),不能替代标识符(identifier)——也就是表名、列名、ORDER BY 后的字段、GROUP BY 表达式等。ORM 内部也受限于此,所以当你需要动态指定这些部分时,框架不会帮你做参数化。
例如以下代码在 SQLAlchemy 或 Django 中都存在风险:
order_field = request.GET.get('sort', 'id')
# 危险!直接拼接列名
query = f"SELECT * FROM posts ORDER BY {order_field} DESC"
可行的缓解方式:
- 用白名单校验
order_field:只允许['id', 'title', 'created_at'] - 避免从请求中读取任意字段名,改用映射表(如
{'date': 'created_at', 'name': 'title'}) - 不要用字符串格式化拼接 SQL 结构,哪怕看起来“只是字段名”
自定义SQL查询中手动拼接条件且未过滤/转义
开发者有时为了性能或复杂逻辑,会在 ORM 里混用原生 SQL 片段,比如构造动态 WHERE 条件。这时候如果依赖 str.replace() 或正则简单过滤单引号,基本无效——攻击者可用 /**/、%00、Unicode 变体绕过。
典型反模式:
-
where_clause = "status = '" + status + "'"→ 然后放进text("SELECT ... WHERE " + where_clause) - 用
mysql_real_escape_string()(已废弃)或自写escape_sql()函数处理输入 - 对 JSON 输入字段不做 schema 校验,直接展开为
IN (...)列表
真正安全的做法是:把每个动态值都作为独立参数传入,哪怕要拆成多个 execute() 调用;或者用 ORM 提供的 query builder(如 Django Q 对象、SQLAlchemy and_())组合条件。
过度信任ORM生成的SQL,忽略日志与错误回显暴露结构
即使 ORM 查询本身是参数化的,一旦开启调试模式或未捕获异常,数据库报错信息(如 MySQL 的 Unknown column 'xxx' in 'field list')可能泄露表结构、字段名甚至数据内容。攻击者可据此反复试探,为后续盲注铺路。
关键点:
- 生产环境必须关闭详细错误输出(Django 的
DEBUG=False,Flask 的PROPAGATE_EXCEPTIONS=False) - 避免在响应体、HTTP Header、日志中直接打印原始 SQL 或异常 traceback
- WAF 或数据库审计日志若记录了带参数的完整 SQL,也可能因配置不当泄露参数值
最常被忽略的是:ORM 安全性完全取决于你怎么用它。一个 .filter(name__icontains=request.GET['q']) 是安全的,但一行 .extra(where=[f"name LIKE '%{q}%'"]) 就足以让整张表裸奔。别假设“用了 ORM 就万事大吉”,得盯住每一处脱离 query builder 的出口。

















