django、sqlalchemy、mybatis等orm的安全边界仅覆盖filter/where/query.filter_by等标准公开api,一旦使用extra()、raw()、text()、${}、动态字段名或原生sql拼接,即越界失效,必须白名单校验结构参数并严格参数化传值。

ORM 的安全边界只覆盖公开 API
Django、SQLAlchemy、MyBatis 这些主流 ORM 框架本身不带 SQL 注入漏洞,但它们的安全保护是有明确边界的:只对 filter、where、query.filter_by 等**标准公开方法**做参数化绑定。一旦你用到内部机制或“逃逸接口”,信任链就断了。
常见越界行为包括:
-
extra()或raw()(Django)里把用户输入拼进 SQL 字符串,哪怕只拼一个字段名 -
text()(SQLAlchemy)里写f"SELECT * FROM {table_name}",而不是纯静态字符串 +params=... - MyBatis 中误用
${}替代#{},导致变量直接展开为 SQL 片段 - 用
Q(eval(...))或filter(**{user_input: value})动态构造查询键,让攻击者传is_superuser__exact或password__contains这类危险字段
动态表名/字段名无法参数化
数据库结构(如表名、列名、ORDER BY 字段、GROUP BY 表达式)在 SQL 解析阶段就必须确定,不能像 WHERE 条件那样走参数绑定。所以任何把用户输入塞进这些位置的操作,本质上都是字符串拼接。
例如:
field = request.GET.get("sort") # 攻击者传 "id; DROP TABLE users --"
queryset = User.objects.order_by(field) # 直接崩
正确做法不是“转义”,而是白名单校验:
- 硬编码允许的排序字段:
allowed_sorts = {"id", "name", "email", "created_at"} - 用
if field not in allowed_sorts: field = "id"拦截非法值 - 不要试图用正则过滤,比如
re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", field)仍可能绕过(如传__dict__或嵌套双下划线操作符)
原生 SQL 接口默认不防注入
ORM 提供的原生执行入口(如 Django 的 connection.cursor().execute()、SQLAlchemy 的 engine.execute(text(...))、MyBatis 的 <script></script> 标签)从设计上就不做自动参数化——它假设你已清楚自己在做什么。
典型错误:
-
cursor.execute(f"SELECT * FROM {table} WHERE id = {user_id}")—— f-string 拼接,100% 中招 -
cursor.execute("SELECT * FROM user WHERE name = %s", [name])—— psycopg2 要求第二参数是tuple,list会抛错,但开发者常忽略报错继续上线 -
text("SELECT * FROM user WHERE id = :id").bindparams(id=user_id)写成text(f"SELECT * FROM user WHERE id = {user_id}"),彻底失效
框架版本与驱动适配常被忽略
不同数据库驱动对占位符语法支持不一致,强行复用代码极易引入漏洞:
- sqlite3 只认
?或:name,传%s不报错但当字面量处理,恶意输入照常执行 - psycopg2 只接受
%s,且第二参数必须是tuple或dict;传[user_id]会 TypeError,但有些项目 catch 之后 fallback 到拼接 - pymysql 默认支持
%s,但不支持%()s命名格式;若开启named=True,又得确保所有参数名都来自白名单
最危险的是开发环境用 sqlite3、生产切到 PostgreSQL —— 测试时没报错,上线后因语法差异直接暴露。
真正难防的从来不是“会不会写参数化”,而是“哪一段代码你以为安全,其实已经越界”。只要用户输入进了 SQL 结构位置(表/字段/ORDER BY/UNION 子句),或者进了未加约束的原生执行链路,ORM 就不再兜底。











