Django框架自带的ORM、CSRF中间件和模板转义机制能挡住大量入门级攻击,但真实生产环境中的漏洞往往出在配置细节、复杂查询拼接和第三方包的误用上。很多开发者以为开了Debug=False、加了CSRF Token就万事大吉,实际上SQL注入、越权访问、敏感信息泄露和文件上传漏洞依然高频出现在Django项目中。下面直接拆解这些问题的具体成因和可落地的防护写法。

ORM查询中的SQL注入盲区

Django ORM的filter、exclude、get等方法对参数做了自动转义,直接用变量传入是安全的。但一旦涉及extra、RawSQL、annotate中的字符串拼接,或者用raw()执行原生SQL时手写格式化,注入风险立刻出现。典型错误写法如下:

from django.db import connection
def unsafe_query(user_input):
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM auth_user WHERE username = '%s'" % user_input)
    return cursor.fetchall()

这种写法中,user_input如果包含单引号和OR 1=1语句,会直接改变SQL逻辑。正确做法是始终使用参数化查询,让数据库驱动负责转义:

from django.db import connection
def safe_query(user_input):
    cursor = connection.cursor()
    cursor.execute("SELECT * FROM auth_user WHERE username = %s", [user_input])
    return cursor.fetchall()

对于必须使用raw()的场景,同样要使用params参数,而不是用f-string或百分号拼接。另外,extra()中select和where参数如果直接嵌入用户输入,也会绕过ORM转义,应尽量避免使用extra,改用annotate配合Q对象或子查询实现。

越权访问与对象级权限缺失

Django自带的Admin后台和基于类的通用视图很容易让开发者忽略对象级权限校验。例如一个查看订单详情的视图,很多代码只判断用户是否登录,却不判断该订单是否属于当前用户:

def order_detail(request, order_id):
    order = Order.objects.get(pk=order_id)
    return render(request, 'order_detail.html', {'order': order})

攻击者只要遍历order_id就能看到其他用户的订单信息。修复方法是在查询时直接绑定用户身份,或者在视图层做显式归属校验。更稳妥的做法是用Django的认证系统配合自定义Manager,让非法ID直接返回404:

def order_detail(request, order_id):
    order = get_object_or_404(Order, pk=order_id, user=request.user)
    return render(request, 'order_detail.html', {'order': order})

对于复杂权限场景,建议引入django-guardian实现对象级权限,或者在模型中定义can_view、can_edit等方法,在视图和模板中统一调用。不要依赖前端隐藏按钮或JavaScript控制访问,后端每个入口都必须独立校验。

敏感信息泄露的多个隐蔽出口

Debug=True在生产环境开启会直接输出堆栈、配置和局部变量,这是最严重的泄露点。但即使Debug=False,Django默认的错误页面和日志仍可能暴露路径、模块名和SQL片段。更常见的泄露来自以下几个地方:

第一,settings.py中的SECRET_KEY、数据库密码、第三方API密钥如果硬编码并提交到代码仓库,等于公开了签名和加密能力。应使用环境变量或独立的secrets管理方案,例如django-environ或python-decouple,并确保settings文件不包含真实密钥。

第二,用户模型默认的User.objects.create_user生成的密码哈希虽然安全,但很多项目在接口返回中不小心序列化了整个User对象,导致password字段被带出。DRF中必须明确使用serializers.ModelSerializer时排除password字段,或者使用专门的输出序列化器。

第三,文件上传功能如果允许用户上传任意文件并直接返回访问URL,可能被用来上传HTML文件实施存储型XSS,或者上传包含恶意脚本的SVG。应在保存文件时限制扩展名白名单,并对上传内容做MIME类型检测,同时将用户上传文件放在独立域名或路径下,避免与主站同源执行脚本。

CSRF防护的常见绕过与配置错误

Django的CsrfViewMiddleware默认对POST、PUT、DELETE等请求强制校验CSRF Token,但以下情况会导致防护失效:

一是使用@csrf_exempt装饰器时范围过大。有些开发者为了调试方便直接在视图函数上关闭CSRF,上线时忘记移除。应尽量避免全局豁免,如果确实有API接口不需要CSRF,应使用基于Token或Session的认证方式替代,而不是直接关闭防护。

二是AJAX请求中忘记传递CSRF Token。虽然Django提供了从Cookie中读取csrftoken并设置到X-CSRFToken请求头的机制,但需要前端代码主动配合。如果前端使用fetch或axios,应统一封装请求拦截器,自动从document.cookie中提取csrftoken并附加到非GET请求。

三是CSRF Token本身通过GET请求泄露。如果某个视图把csrftoken作为URL参数返回,或者日志中记录了完整URL,Token就可能被Referer头或浏览器历史泄露出去。应确保Token只通过Cookie或响应体传递,不在URL中出现。

XSS防护中的模板误用与富文本处理

Django模板默认对变量进行HTML转义,这是防止反射型XSS的基础。但很多开发者为了让富文本正常显示,使用了safe过滤器或mark_safe函数,这等于放弃了转义保护。如果富文本内容来自用户输入,必须经过严格的HTML清洗。推荐使用bleach库,只允许白名单标签和属性:

import bleach
allowed_tags = ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a']
allowed_attrs = {'a': ['href', 'title']}
clean_html = bleach.clean(user_input, tags=allowed_tags, attributes=allowed_attrs, strip=True)

另外,JavaScript代码中嵌入Django变量时,模板转义不会防止JS上下文中的XSS。例如在script标签内使用{{ user_input }},攻击者可以注入</script>标签提前闭合。正确做法是将数据放在data-*属性中,由JavaScript读取,或者使用json_script过滤器安全地序列化数据。

文件上传漏洞的完整防护链

Django的FileField和ImageField本身不校验文件内容,只存储文件并生成路径。攻击者可以上传伪装成.jpg的PHP脚本或包含恶意代码的SVG文件。防护需要多层配合:

第一层是扩展名白名单。在表单或序列化器中校验文件扩展名,只允许jpg、png、pdf、docx等业务需要的类型。第二层是内容检测,使用python-magic读取文件头判断真实MIME类型,防止扩展名伪造。第三层是存储隔离,将用户上传文件保存到单独的文件系统或对象存储桶,并通过独立的域名提供访问,避免同源执行。第四层是限制文件大小和上传频率,防止资源耗尽攻击。第五层是对图片文件进行重编码处理,使用Pillow打开并重新保存,可以消除大部分嵌入在图片元数据或像素中的恶意载荷。

点击劫持与安全响应头配置

Django默认的XFrameOptionsMiddleware会为所有响应设置X-Frame-Options: DENY,防止页面被嵌入iframe。但如果项目使用了django-cors-headers或自定义中间件,可能覆盖或移除了这个头。应检查中间件顺序,确保安全头最后生效。同时建议在settings.py中显式配置以下安全头:

SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

这些配置在HTTPS环境下能有效防止浏览器层面的嗅探、MIME混淆和协议降级攻击。如果项目还在HTTP环境下运行,应优先迁移到HTTPS,否则部分安全头无法生效。

第三方包与依赖链风险

Django生态中的第三方包质量参差不齐,很多漏洞并非来自Django本身,而是来自django-rest-framework、django-allauth、django-cms等常用包的旧版本。应定期使用pip-audit或safety检查依赖中的已知漏洞,并保持Django主版本的安全更新。特别注意那些直接操作数据库或处理用户输入的第三方包,例如django-filter、django-taggit等,它们在自定义查询时可能存在与原生ORM相同的注入风险。

会话管理与认证接口加固

Django默认使用数据库存储会话,Session ID通过Cookie传递。如果SESSION_COOKIE_HTTPONLY未设置为True,JavaScript可以读取Session ID,配合XSS漏洞就能完全劫持用户会话。应在settings.py中显式设置SESSION_COOKIE_HTTPONLY = True,并考虑使用SESSION_COOKIE_SAMESITE = 'Lax'或'Strict'来缓解CSRF。对于登录接口,应添加速率限制,防止暴力破解。可以使用django-axes或自定义中间件记录失败尝试,达到阈值后锁定账号或要求验证码。密码重置接口同样需要速率限制和一次性Token的时效控制,避免Token被重复使用或长期有效。

Django框架本身的安全设计在Web框架中属于第一梯队,但安全是一个持续配置和编码习惯的问题。每个视图的权限校验、每处用户输入的上下文处理、每个第三方包的版本更新,都直接决定线上系统的攻击面。把上面这些具体措施落实到代码审查和部署流程中,比单纯依赖框架默认配置要可靠得多。