ORM框架本身不自动防注入,raw()、text()、DB::raw()、${}、extra()等原生接口绕过参数化机制;字段名、表名、ORDER BY等结构化内容无法参数化,须白名单校验。

现代 ORM 框架本身不“产生”SQL注入,但开发者一旦越过安全使用边界,就等于亲手拆掉参数化防护——raw()、extra()、动态字段名、ORDER BY拼接,都是常见突破口。
哪些 ORM 接口会直接绕过参数化?
Django、SQLAlchemy、MyBatis 等主流 ORM 的标准查询方法(如 filter()、where()、find())默认走预编译 + 参数绑定,天然防注入。但以下接口完全不经过 ORM 查询编译器,等同于直连数据库驱动:
-
raw()(Django)、text()(SQLAlchemy)、DB::raw()(Laravel)、${}(MyBatis XML)——只要 SQL 字符串里有用户输入拼接,就失效 -
extra()(Django)和annotate()中混用RawSQL或字符串模板(如template="%(function)s(..., %(decimals)s)"),decimals若为恶意字符串,会直接进 SQL -
cursor.execute()(原生 DB-API 调用)未用参数占位,而是用f"SELECT * FROM {table}"拼表名
为什么字段名、表名、ORDER BY 不能参数化?
数据库协议只允许「值」参与参数化(即 WHERE name = ?、INSERT INTO ... VALUES (?)),而「结构」部分在 SQL 解析阶段就必须确定,驱动层根本不支持运行时替换。这意味着:
-
ORDER BY {user_input}即使其他所有参数都安全,整条语句仍可被劫持 -
SELECT * FROM {table_name}无法用params=补救,必须白名单校验或配置映射 -
GROUP BY、LIMIT offset、UNION SELECT中的标识符,同样不可参数化
典型错误:把 request.GET.get("sort") 直接传给 order_by(),却没做 if sort not in ["created_at", "score"]: 拦截。
动态字段名(如 filter(**{field: value}))为何危险?
Django 的 filter() 支持字典解包,但键(field)不会被转义或校验。攻击者可传入:
-
__class__、__dict__等模型元属性,触发非预期行为 -
id__in后接子查询字符串(如"id__in=(SELECT password FROM auth_user)"),若后端解析宽松可能执行 - 任意嵌套字段路径(
profile__user__is_staff),若字段未定义或权限未控,可能暴露数据或引发异常
安全做法不是禁用字典解包,而是先白名单过滤 field,例如:if field not in ["title", "author__name", "status"]: 再构造 {field: value}。
最容易被忽略的注入点:非 WHERE 子句上下文
多数人只盯着 WHERE,但注入早已蔓延到更隐蔽的位置:
- PostgreSQL 的
jsonb_path_query(data, '$.items[*] ? (@.name == "' + user_input + '")')—— JSON 路径表达式中的字符串拼接,参数化无效 - MySQL 的
LOAD DATA INFILE '/tmp/{user_file}'—— 文件路径若可控,可读任意文件 - Oracle 的
UTL_HTTP.REQUEST('http://' || user_input)—— 拼接 URL 导致 SSRF - Django
model._meta.db_table或_meta.get_field()被用于构造 SQL 时,若字段名来自请求,也属失控结构
这些场景没有统一的“ORM 安全 API”,只能靠开发人员识别上下文类型,手动做白名单、长度限制或拒绝用户控制结构部分。











