ORM不能百分百防SQL注入,因其仅在标准方法(如filter、where)中启用参数化;调用.raw()、.text()、DB::raw()、${}、extra()、annotate()混用字符串模板、cursor.execute()拼接表名/ORDER BY、动态字段名解包、私有API(如_meta、Func)等均绕过参数化,且表名、列名、排序等结构化部分无法参数化,须白名单校验。

ORM 不能百分之百防 SQL 注入,是因为它只在你“老老实实走标准路径”时才起作用;一旦调用原生 SQL 接口、拼接结构化片段,或误用内部 API,参数化机制立刻失效。
哪些 ORM 接口会直接绕过参数化
主流 ORM 的 filter()、where()、find() 等方法默认启用预编译 + 参数绑定,但以下接口完全跳过 ORM 查询编译器,等同于直连数据库驱动:
-
.raw()(Django)、.text()(SQLAlchemy)、DB::raw()(Laravel)、${}(MyBatis XML)——只要字符串里有用户输入拼接,就失效 -
.extra()(Django)和.annotate()中混用Func()或字符串模板,比如extra={'decimals': request.GET.get('precision')},恶意值会直接进 SQL -
cursor.execute()(Python DB-API)或Statement.executeUpdate()(Java JDBC)未用占位符,而是用f"SELECT * FROM {table}"拼表名
为什么表名、字段名、ORDER BY 无法参数化
数据库协议只允许「值」参与参数化(如 WHERE name = ?),而「结构」部分(表名、列名、ORDER BY、GROUP BY、LIMIT offset)在 SQL 解析阶段就必须确定,驱动层根本不支持运行时替换。
-
ORDER BY {user_input}即使其他所有参数都安全,整条语句仍可被劫持 -
SELECT * FROM {table_name}无法靠params=补救,必须白名单校验或配置映射 - 典型错误:
User.objects.order_by(request.GET.get('sort')),却没做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}。
ORM 私有语法和内部属性的陷阱
_meta、.extra()、.annotate() 配合 Func() 等属于“内部控制层”,不是公开安全 API。它们不校验输入,也不做参数绑定。
-
User.objects.extra(where=["name LIKE '%" + keyword + "%'"])——keyword未过滤即拼入 SQL -
annotate(score=Func('score', function='ROUND', template="%(function)s(%(expressions)s, %(decimals)s)", extra={'decimals': request.GET.get('precision')})—— 若precision是"2); DROP TABLE user; --",会直接注入 - 用
model._meta.db_table拼接日志表名,却没校验model是否为白名单内模型类
真正难的从来不是拼 SQL,而是搞清“谁能在什么条件下看到什么数据”。ORM 帮你挡住了最野的攻击,但业务规则得自己守;最容易被忽略的,是那些不落在 WHERE 里的注入点——比如 jsonb_path_query 的路径参数、extra(tables=["user"]) 引入的额外表名。

















