Django 安全实践:框架给你兜了多少底,你还要补多少
Django 有一个让新人过度自信的特点:它默认帮你做了太多。ORM 自动参数化查询、模板自动 HTML 转义、CSRF 中间件开箱即用——这些都是真实存在的保护。但正因为「默认安全」的口碑太好,很多人把「框架有这个功能」和「我的应用是安全的」画了等号,然后在 DEBUG=True 的生产环境上跑了好几个月。
我想聊的不是漏洞列表,而是一个思维框架:Django 的安全机制分两类——框架替你做的和必须你主动开启的。混淆这两类是大多数 Django 应用安全问题的根源。
框架替你做的部分
这些机制默认开启,你不需要写一行额外代码:
-
ORM 参数化:
User.objects.filter(email=user_input)是安全的,Django 会把参数和 SQL 语句分离传给数据库驱动。危险只在你绕过 ORM 手写raw()时出现——此时参数化的责任回到你身上:User.objects.raw('SELECT * FROM users WHERE email = %s', [email]),注意%s占位,绝不能用 f-string 或字符串拼接。 -
模板自动转义:
{{ user_input }}渲染出来的是 HTML 实体,不是原始 HTML。唯一例外是你显式用了|safe或mark_safe()——这两个函数的语义是「我保证这段内容是安全的」,但如果你把用户输入直接塞进去,等于你替 Django 把这个保证背了下来。 -
CSRF 中间件:只要
django.middleware.csrf.CsrfViewMiddleware在MIDDLEWARE里(默认在),POST 请求就需要 token 验证。但 API 项目常见做法是把某些 view 标@csrf_exempt,这个装饰器是安全豁免,不是安全增强,用之前要想清楚替代方案(比如 JWT 认证)。
必须你主动配置的部分
这些是「框架提供了功能,但默认不开或默认不够」的区域。
生产环境配置清单——没有这些,其他做得再好也是纸上谈兵:
DEBUG = False # 从不在生产打开
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') # 从环境变量读,绝不硬编码
ALLOWED_HOSTS = ['yourdomain.com']
# HTTPS 强制
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
# Cookie 安全
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
# 防点击劫持
X_FRAME_OPTIONS = 'DENY'
SECURE_CONTENT_TYPE_NOSNIFF = True这些配置为零的项目我见过不少,尤其是 DEBUG = True 上线——Django 的 debug 页面会把完整的环境变量、数据库配置、源码路径全部暴露出来,攻击者看一眼就够了。
密码哈希升级:Django 默认用 PBKDF2,够用但不算强。如果想用 Argon2(当前推荐的密码哈希算法,赢得了 Password Hashing Competition),需要安装 argon2-cffi 并显式配置 PASSWORD_HASHERS:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher', # 向下兼容旧密码
]文件上传验证:Django 的 FileField 不做内容校验,只存路径。扩展名验证、文件大小限制、MIME 类型校验,需要你自己写 validator。更重要的是,用户上传的文件绝不能和应用代码放在同一个可执行路径下,生产环境建议直接扔到 S3 或对象存储,通过预签名 URL 访问,完全隔离执行权限。
认证与授权:最容易漏的一层
SQL 注入、XSS、CSRF——大多数人知道要防。但对象级权限(Object-Level Permission)是另一回事。
一个常见的漏洞模式:
# 这段代码有问题
def update_post(request, post_id):
post = Post.objects.get(id=post_id)
# 只验证了「登录」,没验证「是不是这篇文章的作者」
if not request.user.is_authenticated:
return HttpResponseForbidden()
post.title = request.data['title']
post.save()正确做法是在 queryset 层就加上所有权过滤:
def update_post(request, post_id):
post = get_object_or_404(Post, id=post_id, author=request.user)
# 如果 post_id 不属于当前用户,直接 404,而不是 403
...用 DRF 的话,把对象级权限封装成 BasePermission 子类,在 has_object_permission 里判断,比每个 view 手写检查逻辑要可靠得多。
把安全敏感的配置集中在 settings/production.py 里显式声明,而不是依赖「Django 默认就是这样」。框架的默认值是为开发体验优化的,生产配置必须你亲手设定。
一句话心法
「框架帮你做了」不等于「你不需要做」——Django 的安全护城河是真实的,但它只保护你不踩已知的坑;你搭的梯子递给攻击者,框架拦不住。