ORM安全边界仅限标准查询方法(如filter、where),调用raw()、text()等原生接口即退出安全区;表名、排序字段等结构部分无法参数化,须白名单校验。

ORM 的安全边界只在“标准查询路径”内有效
ORM 框架本身不防注入,它只在你走 filter()、where()、find() 这类封装好的方法时,才默认启用参数化。一旦调用 raw()、text()、execute()、DB::raw() 或 MyBatis 的 ${},就等于主动退出安全区——SQL 字符串由你全权负责,数据库驱动不会帮你做任何转义或占位。
哪些 RawSQL 调用实际等于手动拼 SQL
这些写法看着像“用了 ORM”,实则和直接写 JDBC Statement 一样危险:
-
Model.objects.raw("SELECT * FROM user WHERE name = '" + request.GET.get('name') + "'")—— 字符串拼接,'直接逃逸 -
session.execute(text("UPDATE logs SET level = '" + level + "'"))—— 即便用了text(),没加:param占位符,仍是硬拼 -
DB::select(DB::raw("SELECT * FROM users WHERE email = '$email'"))(Laravel)—— PHP 变量插值发生在 PHP 层,SQL 已成型 -
<script>SELECT * FROM user WHERE name = '${name}'</script>(MyBatis)——${}是字符串替换,#{}才是预编译
ORDER BY / GROUP BY / 表名/列名不能参数化,但很多人硬塞
数据库协议本身不支持对排序字段、分组字段或标识符(如表名)做参数化绑定。所以以下写法注定不安全:
-
query.order_by(request.GET.get('sort'))—— 若传入id; DROP TABLE users--,多数 ORM 不拦截 -
text(f"SELECT * FROM {table_name}")—— f-string 拼表名,table_name来自用户?零防护 -
WHERE status IN (" + ",".join(status_list) + ")"—— 即便每个status是字符串,拼进括号也绕过参数机制
真正可行的解法是白名单校验:if sort_field not in ['created_at', 'updated_at', 'score']: 直接拒掉;或用 getattr(User, field) 动态取列对象,而非拼字符串。
你以为用了参数,其实参数根本没生效
常见错觉:“我传了参数,所以安全”。但只要 SQL 字符串里已经包含用户输入,参数就只是摆设:
-
session.execute(text(f"SELECT * FROM logs WHERE level = '{level}'"), {"level": "error"})——f""在 Python 层就拼完了,{"level": ...}完全没机会介入 -
cursor.execute("INSERT INTO t VALUES ('" + name + "', " + age + ")")—— 即便age是数字,name的单引号仍可闭合语句 -
MyBatis——bind用+拼,仍是运行时字符串拼接
最易被忽略的一点:ORM 安全性完全取决于你是否让 SQL 结构和用户输入彻底分离。只要结构部分(比如哪张表、按哪列排序、用什么关键字)来自不可信源,哪怕其余都用了参数,整条语句就已失守。











