select_related通过JOIN优化一对一/多对一查询,prefetch_related用分步查询+Python关联优化多对多/反向关系,二者均由ORM安全生成SQL,避免手写JOIN导致的注入风险。

用 Django 的 select_related 和 prefetch_related 避免手写 JOIN
多表关联最危险的起点,就是有人为“省一次查询”手动拼接 SQL 字符串,比如:"SELECT u.name, p.title FROM auth_user u JOIN blog_post p ON u.id = p.author_id WHERE u.username = '" + username + "'"。这种写法哪怕只漏掉一个单引号转义,就直接触发注入。Django 的 select_related 和 prefetch_related 本质是让 ORM 控制 JOIN 逻辑——字段名、表名、ON 条件全由模型定义决定,用户只传值,不碰结构。
常见错误现象:有人看到 Post.objects.select_related('author').filter(author__username='alice') 就觉得“还是在拼条件”,其实整个 SQL 是 ORM 构建表达式树后生成的,author__username 是字段路径,不是字符串插值;底层执行时,'alice' 一定走参数绑定(如 %s 占位符)。
-
select_related适用于外键/一对一正向关联,生成 INNER JOIN 或 LEFT JOIN,一次查出所有字段 -
prefetch_related适用于多对多、反向 ForeignKey,先查主表,再用 IN 子查询批量取关联数据,避免 N+1 - 两者都不接受动态表名或字段名作为参数;若需按请求决定关联哪张表(比如
request.GET.get('include')),必须白名单校验:if include not in ['author', 'tags', 'category']
警惕 values() 和 values_list() 中的字段名动态传入
关联查询中常需要动态指定返回哪些字段,比如 Post.objects.select_related('author').values('title', 'author__name')。这里 'author__name' 是硬编码字段路径,安全;但一旦改成 .values(*fields),而 fields 来自用户输入,风险立刻出现。
使用场景:后台导出接口支持用户勾选要导出的字段,fields = request.GET.getlist('fields') 直接传进 values() 就等于把字段名交给了 SQL 结构层。
- 必须校验每个字段是否真实存在于模型中:
if not hasattr(Post, field.split('__')[0])或更稳妥地用Post._meta.get_field(field.split('__')[0]) - 禁止允许任意嵌套,比如
'author__profile__phone'若 profile 字段未在模型中定义,get_field会抛异常,但得提前拦截 - 字段名里不能含空格、点号、括号等 SQL 元字符;白名单比黑名单更可靠
别在 Q() 对象里拼接字段名字符串
复杂搜索条件经常用 Q 组合多个条件,例如:Q(title__icontains=kw) | Q(author__username__icontains=kw)。这很安全,因为字段名是 Python 代码字面量。但有人会写成:Q('{}__icontains'.format(field) = kw) —— 这语法根本通不过 Python 解析,所以实际中更多见的是:Q(**{field + '__icontains': kw})。
问题在于 field 若来自请求且未经校验,攻击者传入 author__id__gt 还算可控,但传入 author__id); DROP TABLE auth_user; -- 就可能被 ORM 当作字段前缀处理,最终生成非法 SQL(取决于 ORM 版本和数据库驱动)。
- 必须在解包前做字段白名单检查:
if field not in ['title', 'author__username', 'tags__name'] - 避免用
getattr()动态获取字段对象后再构造Q,因为getattr不校验字段是否可查询(比如私有字段、ForeignKey的_id后缀字段) - 如果真要支持任意字段搜索,优先走全文检索(
SearchVector)或 Elasticsearch,而非开放字段名输入
raw() 查询中关联逻辑必须用参数化,且禁止拼接表名
极少数场景绕不开 raw(),比如跨库 JOIN 或调用存储过程。这时关联条件仍要参数化,但很多人忽略:表名、字段名、JOIN 类型这些结构元素,ORM 不保护,必须人工守门。
错误示例:Post.objects.raw("SELECT * FROM blog_post p JOIN auth_user u ON p.author_id = u.id WHERE u.username = %s AND p.status = '%s'" % (username, status)) —— 第二个 %s 在字符串拼接里,没进参数列表,直接执行。
-
raw()的参数只能通过第二个参数传入:.raw("WHERE u.username = %s", [username]) - 表别名(
p,u)可以硬编码,但主表名(blog_post)若需动态,必须映射到白名单:table_map = {'posts': 'blog_post', 'users': 'auth_user'} - 永远不要用
format()或f-string拼接任何 SQL 结构部分,包括 ORDER BY 字段、GROUP BY 表达式
真正容易被忽略的点是:关联查询的安全边界不在“值”而在“结构”。ORM 能确保 username 不被当代码执行,但无法阻止你把 auth_user 表名替换成 pg_shadow(PostgreSQL 系统表)。这类控制权始终在开发者手上,没捷径可抄。

















