raw() 必须用 params= 传参且仅用于值绑定,表名/字段名等标识符需白名单校验;字符串拼接用户输入会导致sql注入。

raw() 必须用 params= 传参,不能字符串拼接
Django 的 raw() 方法本身不校验 SQL 结构,只负责把 params 列表里的值安全绑定到占位符上。一旦你用 f-string、% 或 .format() 拼接用户输入进 SQL 字符串,就等于绕过所有防护。
常见错误现象:Person.objects.raw(f"SELECT * FROM person WHERE name = '{name}'") —— 单引号一加,攻击者输 ' OR '1'='1 就能查出全表。
- 正确写法必须是:
Person.objects.raw("SELECT * FROM person WHERE name = %s", params=[name]) -
params必须是列表或元组,不能是单个字符串(否则会被当字符序列拆开) - 多个参数按顺序填入,比如
"WHERE name = %s AND age > %s"对应params=["Alice", 18] - MySQL 和 PostgreSQL 都支持
%s占位符,Django 会自动适配底层驱动
动态表名/字段名必须白名单校验
raw() 的 SQL 字符串里,任何标识符(表名、字段名、函数名)都不能来自用户输入。因为 params 只替换占位符,不转义标识符 —— 这不是 Django 的限制,是 SQL 协议本身的约束。
使用场景:前端传参 ?table=logs&sort=created_at,后端拼成 SELECT * FROM {table} ORDER BY {sort}?危险。
- 必须先定义白名单:
ALLOWED_TABLES = {"users", "orders", "products"},再检查if table not in ALLOWED_TABLES: raise ValueError - 字段名同理:
ALLOWED_SORT_FIELDS = {"id", "created_at", "-updated_at"},注意带-的降序也要显式列入 - 不要试图用
re.sub(r'[^a-zA-Z0-9_]', '', user_input)过滤——下划线、数字、大小写组合仍可能撞上敏感字段(如_state、__class__)
避免在 raw() 中嵌套用户控制的子查询或表达式
有人以为 “只对值参数化就够了”,结果在 raw() 里写:RawSQL("SELECT id FROM ({} ) AS t".format(subquery)) —— 这里 subquery 若含用户输入,params 完全无效。
错误写法示例:annotate(score=RawSQL(f"COALESCE({user_col}, 0)")),哪怕后面加 params=[...],{user_col} 已经被拼进 SQL 结构里了。
- 优先改用 ORM 原生表达式,比如
F("field_name")、Coalesce(F("col"), 0) - 实在要拼结构,必须白名单校验字段是否存在且可读:
if not hasattr(MyModel, user_col): raise PermissionDenied - 禁止把
request.GET.dict()直接解包进 SQL 字符串,哪怕只用来生成WHERE条件
cursor.execute() 同样适用,但更易踩坑
直接用 connection.cursor() 执行原生 SQL 时,规则和 raw() 一致,但更容易漏掉 params 或误用字符串格式化。
常见错误现象:调用 cursor.execute("UPDATE users SET status = '" + status + "'"),status 是 "active'; DROP TABLE users; --",某些数据库(如 MySQL 开启 multiple-statements)真会执行第二条。
- 必须写成:
cursor.execute("UPDATE users SET status = %s WHERE id = %s", [status, user_id]) - 执行完记得
cursor.close(),尤其在长连接或异步视图中,避免游标泄漏 - 如果 SQL 里有固定部分(如硬编码表名),也建议提取为常量,和用户变量严格分离,降低混淆风险
实际中最容易被忽略的,是把“用了 raw()” 当成安全信号,却在 SQL 字符串里悄悄拼接了字段、表名、甚至 ORDER BY 子句——这些地方 params 根本不起作用,得靠白名单卡死。











