正确做法是raw()必须用params=传参且仅支持%s占位符,不可字符串拼接;extra()的where等参数同样需params绑定,RawSQL混用用户输入须手动参数化,结构化参数如字段名须白名单校验。
Django 的 raw() 完全跳过 ORM 编译器,SQL 字符串由你全权负责。只要出现 "%s" % user_input 或 f"WHERE name = '{name}'",就等于把数据库钥匙交出去。
raw() 只认位置占位符 %s,所有数据库后端(PostgreSQL/MySQL/SQLite)都兼容;命名参数如 %(name)s 在 SQLite 下会报错params 必须是列表或元组,不能是字典(除非明确只跑 PostgreSQL 且确认驱动支持)User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'")
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", params=[username])
extra() 看似像 filter(),但它内部的 where、tables、joins 都是原始字符串拼接点,不经过任何参数化处理。
extra(where=["status = %s"], params=[user_status]) 是唯一安全路径extra(where=[f"status = '{user_status}'"]) 和裸写 SQL 没区别,直接触发注入connection.ops.adapt_unknown_value() 手动转义——难维护、易漏、不跨数据库Q() 组合,或提前定义好字段白名单映射RawSQL 用在 annotate() 或 filter() 中时,它本身不继承外层 QuerySet 的参数化机制,params= 只绑定它自己 SQL 内的占位符。
annotate(score=RawSQL(f"CASE WHEN title LIKE '%{search}%' THEN 1 ELSE 0 END")) —— search 被直接插进字符串RawSQL("CASE WHEN title LIKE %s THEN 1 ELSE 0 END", params=[f"%{search}%"])
%、_ 会被数据库当语法执行,不是注入但属逻辑漏洞,需用 escape 参数或 __icontains 替代Case(When(title__icontains=search, then=1), output_field=IntegerField())
绕过 ORM 直接用 connection.cursor() 时,Django 不提供任何防护,完全依赖底层驱动的参数化能力。
立即学习“Python免费学习笔记(深入)”;
cursor.execute("SELECT * FROM user WHERE id = %s", [user_id])
cursor.execute(f"SELECT * FROM user WHERE id = {user_id}") 或 "%s" % user_id
%s,PostgreSQL 支持 %s 和 %(name)s,但混用会导致迁移失败%s 最保险结构参数永远不被保护——表名、字段名、ORDER BY、GROUP BY、函数名这些,哪怕你用了 params= 也救不了。它们不属于“值”,而是 SQL 语法骨架,必须靠白名单校验或静态定义兜底。