ORM 通常是安全的,但 Django 也提供了 raw()、cursor、extra() 这些让你自己写 SQL 的口子。这篇在自己的本地表上用同一个输入,把安全写法与危险写法的结果并排摆出来。
发给数据库的 SQL : WHERE "lab_person"."name" = %s
参数(单独传递): ("x' OR '1'='1",)
返回行数: 0 (没有叫这个名字的人)用 connection.execute_wrapper 截下来的 SQL 里是 %s 占位符,参数在第二个位置单独传递——这就是 ORM 防注入的原理。
f-string 拼接 → 返回 3 行(全部): ['alice', 'bob', 'carol'] 参数化 %s → 返回 0 行
拼接时输入里的单引号结束了字符串、OR '1'='1' 变成永真条件。这里的数据是我自己建的 3 行本地数据,只演示「条件被改写」,没有做任何破坏性操作。
cursor f-string → 3 行 cursor 参数化 → 0 行 ORDER BY name DESC(列名写在 SQL 里) → ['carol', 'bob', 'alice'] ORDER BY %s DESC,参数 'name'(当作字符串) → ['alice', 'bob', 'carol'] ← 与插入顺序相同,没有按 name 排序:参数是值,不是列名
order_by('name' ) → ['alice', 'bob', 'carol']
order_by('-name' ) → ['carol', 'bob', 'alice']
order_by('secret' ) → ['bob', 'carol', 'alice']
order_by('name; DROP TABLE lab_person' ) → FieldError: Cannot resolve keyword 'name; DROP TABLE lab_person' into field. Choic
order_by('nope' ) → FieldError: Cannot resolve keyword 'nope' into field. Choices are: id, name, secre
白名单之后 safe_order("secret") = name这不是 SQL 注入(Django 会校验字段名),但如果排序参数直接来自请求,用户可以通过排序结果推断出你没想暴露的字段(比如 secret 列的大小关系)。
extra(where=[f-string]) → 3 行 extra(where, params) → 0 行
extra() 和 RawSQL 一样,是「你负责安全」的接口;能用 ORM 表达的查询就不要用它们。