在Django项目中使用raw()方法执行原生SQL时,最核心的安全问题就是SQL注入。解决这个问题的关键方法是:永远不要用字符串拼接的方式把用户输入塞进SQL语句里,而是使用参数化查询(parameterized query),通过raw()方法的params参数或者%s占位符来传递变量。具体来说,你应该写成Model.objects.raw('SELECT * FROM app_table WHERE id = %s', [user_input])这种形式,而不是写成Model.objects.raw(f'SELECT * FROM app_table WHERE id = {user_input}')。前者是安全的,后者就是一个巨大的SQL注入漏洞。
Django ORM本身已经帮你做了大部分防注入工作,但raw()是一个"逃生舱",它允许你绕过ORM直接写SQL。一旦你打开了这个口子,如果不注意写法,就等于把数据库的大门敞开了。下面我会从原理、具体写法、常见陷阱、高级封装方案几个层面,把这件事讲透。
为什么raw()方法存在SQL注入风险Django ORM的常规查询,比如Model.objects.filter(name=user_input),底层会自动把参数转义处理。但raw()方法是直接执行你写的SQL字符串,Django不会对SQL语句本身做任何语义分析和转义。它只负责把你给的SQL发给数据库执行。如果你在SQL字符串里用了f-string、format()或者%字符串格式化把用户输入拼进去,那用户输入的内容就会被当作SQL语法的一部分来解析。攻击者可以输入类似"1 OR 1=1"这样的内容,让你的查询逻辑完全失控。
举个最典型的危险写法:
# 危险!绝对不要这样写
user_id = request.GET.get('id')
queryset = User.objects.raw(f'SELECT * FROM auth_user WHERE id = {user_id}')
如果用户传入的id是"1; DROP TABLE auth_user; --",那后果不堪设想。数据库会忠实地执行这条拼接后的SQL,先查id=1,然后执行删除表的操作。这不是理论风险,这是实实在在会被利用的漏洞。
参数化查询:raw()的正确安全写法Django的raw()方法支持两种参数传递方式,都是安全的。第一种是用列表或元组传参,配合%s占位符:
# 安全写法一:使用列表传参
user_id = request.GET.get('id')
queryset = User.objects.raw('SELECT * FROM auth_user WHERE id = %s', [user_id])
第二种是使用字典传参,配合%(name)s这种命名占位符:
# 安全写法二:使用字典传参
user_id = request.GET.get('id')
username = request.GET.get('name')
queryset = User.objects.raw(
'SELECT * FROM auth_user WHERE id = %(uid)s AND username = %(uname)s',
{'uid': user_id, 'uname': username}
)
这两种写法的本质是一样的:用户输入永远不会被拼接到SQL字符串里,而是作为独立的参数传递给数据库驱动。数据库驱动会在执行前对参数做转义处理,确保它只被当作数据值,而不是SQL代码。这就是参数化查询的核心原理。
raw()中params参数的使用细节除了上面的写法,raw()方法还有一个params参数,可以直接传一个列表或元组:
# 使用params参数
queryset = Model.objects.raw('SELECT * FROM app_model WHERE status = %s AND created > %s', params=[status_val, date_val])
需要注意几个细节。第一,占位符的数量必须和params里的元素数量完全一致,少了会报错,多了也会报错。第二,如果你的SQL里有多个相同的参数需要重复使用,不能只传一个值,必须在params里重复放。比如:
# 同一个参数用两次,需要传两次
category = request.GET.get('category')
queryset = Product.objects.raw(
'SELECT * FROM app_product WHERE category = %s OR parent_category = %s',
params=[category, category]
)
第三,如果你用的是PostgreSQL,占位符风格可能需要调整为%s(Django会自动处理),但如果你在raw()里写的是特定数据库的原生语法,比如PostgreSQL的$1、$2,那就不能混用%s占位符了。保持一致性很重要。
封装一个安全的raw()工具函数在实际项目中,如果你频繁使用raw(),建议封装一个统一的安全函数,避免团队成员写出不安全的代码。下面是一个我推荐的封装方案:
from django.db import connection
def safe_raw(model, sql, params=None):
"""
安全的raw()包装函数
:param model: Django模型类
:param sql: SQL语句字符串,使用%s占位符
:param params: 参数列表或元组
:return: QuerySet
"""
if params is None:
params = []
if not isinstance(params, (list, tuple)):
raise TypeError("params必须是列表或元组")
# 检查占位符数量和参数数量是否匹配
placeholder_count = sql.count('%s')
if placeholder_count != len(params):
raise ValueError(f"占位符数量({placeholder_count})与参数数量({len(params)})不匹配")
return model.objects.raw(sql, params)
这个函数做了三件事:强制要求params必须是列表或元组、检查占位符和参数数量是否匹配、统一调用raw()。团队里所有人都用这个函数,就能从制度层面杜绝字符串拼接的写法。你还可以在函数里加上日志记录,方便后续审计。
raw()配合连接查询时的安全注意事项很多人用raw()是因为要做复杂的JOIN或者子查询,这时候更容易犯错。比如你要做一个多表关联查询:
# 安全的多表JOIN写法
keyword = request.GET.get('keyword')
min_price = request.GET.get('min_price', 0)
queryset = Product.objects.raw('''
SELECT p.*, c.name as category_name
FROM app_product p
INNER JOIN app_category c ON p.category_id = c.id
WHERE p.name LIKE %s AND p.price >= %s
''', params=[f'%{keyword}%', min_price])
这里有一个容易忽略的点:LIKE查询里的通配符%不应该由用户输入,而应该在代码里拼接好再传参。上面的写法是先在Python里用f-string拼好'%{keyword}%',然后把整个字符串作为一个参数传进去。这是安全的,因为f-string是在Python层面做的字符串拼接,不是在SQL层面。但如果你写成WHERE p.name LIKE '%' + %s + '%'这种SQL层面的拼接,就有问题了——虽然大部分数据库驱动能处理,但不推荐,风格也不统一。
raw()返回结果的处理和类型转换raw()返回的是一个RawQuerySet,它和普通QuerySet有一些区别。最明显的是,raw()返回的模型实例不会自动保存到数据库(因为它可能来自多表查询,不一定对应单一模型表),而且你不能对它调用.save()方法。如果你需要修改数据,应该用普通的ORM查询找到对象再修改。
# raw()结果的遍历和使用
results = safe_raw(User, 'SELECT id, username FROM auth_user WHERE is_active = %s', [True])
for user in results:
print(user.id, user.username) # 可以通过属性访问
# user.save() # 这行会报错!不要这样做
另外,如果你的raw()查询SELECT了非模型字段(比如聚合函数的结果),这些字段也会作为属性挂在实例上,但不会有对应的模型字段定义。访问它们不会报错,但要注意类型可能是Decimal、datetime等需要转换的类型。
什么时候应该用raw(),什么时候不应该说句实话,很多人用raw()其实是因为不熟悉ORM的高级用法。Django ORM的annotate()、aggregate()、F表达式、Q对象、Subquery、Window函数等功能已经非常强大,绝大多数场景都不需要raw()。只有在以下几种情况才建议用raw():
第一,需要使用数据库特有的函数,比如PostgreSQL的JSON操作函数、全文搜索函数、地理空间函数等。第二,需要执行极其复杂的SQL,ORM实在表达不了,比如递归CTE、窗口函数的复杂分区等。第三,需要做性能调优,手写SQL比ORM生成的更高效(但这种情况很少,先用ORM再优化才是正道)。
如果你只是想做一个简单的条件过滤或者排序,请优先用ORM。用raw()就意味着你要自己承担安全责任和维护成本。
额外的安全加固措施除了参数化查询,还有几个层面可以加固:
第一,输入验证。在把用户输入传给raw()之前,先做类型和格式校验。比如id应该是整数,就先用int()转换或者用正则校验。这不是防SQL注入的(参数化查询已经防了),但能防逻辑错误和意外输入。
# 输入验证示例
try:
user_id = int(request.GET.get('id'))
except (ValueError, TypeError):
return HttpResponseBadRequest("无效的ID")
queryset = safe_raw(User, 'SELECT * FROM auth_user WHERE id = %s', [user_id])
第二,权限控制。即使SQL本身是安全的,也要确保当前用户有权访问这些数据。在视图层做好权限检查,不要因为用了raw()就忽略业务逻辑层的权限控制。
第三,最小权限原则。数据库连接使用的账号应该只有必要的权限,不要用超级管理员账号跑应用。这样即使出了问题,损失也能控制在最小范围。
第四,代码审查。把raw()的使用作为代码审查的重点关注项,任何出现字符串拼接SQL的代码都应该被打回重写。
总结:安全使用raw()的核心原则把所有要点浓缩成几条铁律:永远用%s占位符加params传参,永远不要用f-string或format拼接用户输入到SQL里;封装统一的安全函数让团队遵守;能用ORM就不用raw();输入验证和权限控制不能少;代码审查要盯紧。做到这些,你的Django项目在使用raw()时就不会有SQL注入的问题。安全不是靠运气,是靠规范和习惯。
