Raw SQL是ORM安全边界的断裂点:.raw()、.execute()等原生接口使ORM退化为字符串拼接执行器,参数化失效;表名、字段名、ORDER BY等结构部分无法参数化,须白名单校验;.extra()、.annotate()配合Func()及_meta属内部控制层,不绑定参数、无注入防护。

Raw SQL 是 ORM 安全边界的断裂点
Django ORM、SQLAlchemy 或 TypeORM 的标准查询方法(如 .filter()、.where()、.find())默认启用参数化查询,天然免疫常规 SQL 注入。但一旦调用 .raw()、.execute()、text() 或 query() 这类原生 SQL 接口,ORM 就退化为“字符串拼接执行器”——安全机制完全失效。
常见错误现象包括:
- 用户输入直接插进
.raw("SELECT * FROM user WHERE name = '" + name + "'"),攻击者输' OR 1=1 --即可绕过条件 - 用 f-string 拼接表名或排序字段:
f"ORDER BY {order_field}",哪怕其他参数都用了占位符,整个查询仍被劫持 - 在 MyBatis 的 XML 中误用
${}替代#{},导致字段名或值未绑定而直插 SQL
哪些参数能参数化,哪些必须白名单校验
数据库驱动只允许「值」参与参数化,即 WHERE、INSERT VALUES、UPDATE SET 后的表达式;但「结构」部分(表名、字段名、ORDER BY、GROUP BY、LIMIT offset)无法用占位符,必须硬编码或白名单过滤。
实操建议:
-
ORDER BY字段不能来自 request 参数直传,应限定为['created_at', 'name', 'score']等预设列表,用if order_field not in ALLOWED_SORT_FIELDS: raise ValueError拦截 - 动态表名(如分表场景)禁止拼接,改用配置映射:
table_map = {'2026Q1': 'user_2026_q1'},再查表名是否在 key 中 - PostgreSQL 的
jsonb_path_query等函数参数若含用户输入,需先转义再拼,或改用 ORM 提供的 JSON 查询接口
Django ORM 的私有语法陷阱:_meta 和 extra()
Django 的 .extra()、.annotate() 配合 Func()、或直接访问模型 _meta.db_table,都属于“内部控制层”,不是公开安全 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')})——decimals若为字符串"2); DROP TABLE user; --",会直接注入 - 用
model._meta.db_table拼接日志表名,却没校验model是否为白名单内模型类
ORM 并不自动处理存储过程和动态 EXEC
即使你用的是 SQLAlchemy,如果最终执行的是 CALL proc_name(?),而存储过程内部用 EXEC(@sql) 拼接并执行字符串,那 ORM 层的参数化毫无意义——风险转移到了数据库侧。
这类场景必须同步检查:
- 存储过程中是否出现
sp_executesql、EXEC(@dynamic_sql)、EXECUTE IMMEDIATE(Oracle)等动态执行语句 - 传入存储过程的参数,是否在过程体内被用于字符串拼接而非参数绑定
- MySQL 的
PREPARE ... EXECUTE若基于用户输入构造 SQL 字符串,同样不可信
真正难防的从来不是用户输什么,而是开发者在哪一刻忘了“这个字符串要进 SQL”。Raw SQL、动态结构、存储过程内拼接——这三处是 ORM 安全幻觉最容易破灭的地方。

















