django安全最佳实践 - 防护常见Web安全漏洞

📂 所属阶段:第三部分 — 高级主题
🎯 难度等级:高级
⏰ 预计学习时间:3-4小时
🎒 前置知识:用户认证系统

目录


安全概述

Web安全是一道不能触碰的红线。好在django提供了强大的内置防护,但前提是——我们必须正确配置和使用。本节将带你快速理解最需要关注的几类攻击,以及django是怎么应对的。

和django最相关的OWASP Top 5

  • 注入攻击:django ORM防止SQL注入,模板自动转义防止XSS
  • 失效的访问控制:通过装饰器和权限系统来控制访问
  • 身份验证失败:内建的认证中间件和密码验证器保驾护航
  • 安全配置错误:内置 check --deploy 一键发现配置问题
  • CSRF:原生的CSRF中间件直接防御跨站请求伪造

核心安全原则

  1. 最小权限:只给用户和进程最必要的权限,避免随意使用超级用户
  2. 输入白名单,输出全转义:不要用模糊过滤代替白名单,所有输出到页面的数据必须转义
  3. 深度防御:层层设防,就算一层被突破,后面还有保护
  4. 不暴露敏感信息:错误页面、日志中禁止泄露密钥、路径等敏感数据

XSS防护

跨站脚本(XSS)是最常见的Web攻击之一。简单说,就是攻击者在页面里注入恶意脚本,当其他用户访问时脚本被执行,从而窃取信息或冒充身份。

1. 模板自动转义——第一道防线

django模板引擎默认会转义所有HTML特殊字符,把 < 变成 &lt;,这样脚本就不会被执行。我们唯一要做的就是不要随便禁用这个保护

错误用法

随意使用 |safe 过滤器就是在开门揖盗:

<!-- 危险!不要这样 -->
<div>{{ user_comment|safe }}</div>
正确用法

保持自动转义,或者明确使用 escape 过滤器,让输出安全落地:

<!-- 安全:自动转义 -->
<div>{{ user_comment }}</div>

<!-- 显式转义(效果相同) -->
<div>{{ user_comment|escape }}</div>

2. 富文本怎么处理?用 bleach

有些场景确实需要接收用户输入的富文本,比如评论可以包含粗体、换行等。这时候不能简单放开 safe,而应该用专门的清理库,比如 bleach

安装 bleach:

pip install bleach

创建自定义模板过滤器,只允许安全的标签和属性:

# myapp/templatetags/safe_tags.py
import bleach
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

@register.filter
def safe_html(text):
    allowed_tags = ['p', 'br', 'strong', 'em', 'u', 'ol', 'ul', 'li', 'code', 'pre']
    allowed_attrs = {'code': ['class']}
    cleaned = bleach.clean(text, tags=allowed_tags, attributes=allowed_attrs, styles=[], strip=True)
    return mark_safe(cleaned)

模板中使用时,加载过滤器并调用即可:

{% load safe_tags %}
<div>{{ user_comment|safe_html }}</div>

3. 内容安全策略(CSP)

CSP是一道由浏览器执行的额外防线,通过HTTP响应头告诉浏览器:哪些来源的资源可以加载,哪些脚本可以执行。django内置了部分支持。

最基础的配置就是启用 SecurityMiddleware 并开启几个安全头:

# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',  # django内置,一键配置部分安全头
    # 其他中间件...
]

# 一键启用部分安全头
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True

更完整的CSP策略,可以通过自定义中间件或者在反向代理(如Nginx)层配置,这样可以精细控制每一个资源类型。


CSRF防护

跨站请求伪造(CSRF)利用的是用户已登录的身份,在用户不知情的情况下,诱导浏览器发出恶意请求。django的原生CSRF中间件可以防住绝大多数情况。

1. 中间件不要动

确保 django.middleware.csrf.CsrfViewMiddlewareMIDDLEWARE 中,而且顺序正确。它默认就是开启的,千万不要去掉。

2. 表单里带上CSRF令牌

所有通过 POST 请求提交的表单,都要在里面加上 {% csrf_token %}

<form method="post">
    {% csrf_token %}
    <input type="text" name="comment">
    <button type="submit">提交</button>
</form>

3. AJAX请求也别忘了令牌

前端用 Axios 时,可以自动带上CSRF令牌。先从Cookie里取,然后设置到请求头里:

// 从Cookie获取csrftoken
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

// 配置Axios
import axios from 'axios';
axios.defaults.xsrfCookieName = 'csrftoken';
axios.defaults.xsrfHeaderName = 'X-CSRFToken';

4. 关键 settings 加固

生产环境一定要收紧配置:

# settings.py
CSRF_COOKIE_SECURE = True              # 仅通过HTTPS传输cookie
CSRF_COOKIE_SAMESITE = 'Strict'        # 严格限制跨站请求携带cookie
CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com']  # 信任的来源

SQL注入防护

SQL注入通过拼接恶意SQL语句来操控数据库,可能导致数据泄露甚至服务器被控制。django ORM 是防 SQL 注入的基石。

1. 绝对不要拼接 SQL

下面这种操作就是在亲手制造漏洞: ::: danger 绝对不要这样

# 危险!SQL拼接
username = request.GET.get('username')
user = User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'")

:::

2. 正确使用 ORM 和参数化

所有查询都应该通过 ORM 或者参数化的原生 SQL 来进行: ::: tip 安全用法

# 安全:ORM 自动参数化
username = request.GET.get('username')
user = User.objects.filter(username=username).first()

# 原生 SQL 也必须参数化
user = User.objects.raw("SELECT * FROM auth_user WHERE username = %s", [username])

# cursor同样需要参数化
from django.db import connection
with connection.cursor() as cursor:
    cursor.execute("SELECT * FROM auth_user WHERE username = %s", [username])
    user = cursor.fetchone()

:::

这些写法能确保用户输入被当作数据,而不是被解释为SQL代码,从根源上杜绝注入。


其他核心安全防护

1. 点击劫持防护

点击劫持攻击会把你的页面藏在一个透明的 iframe 里,诱导用户点击。django 内置了 XFrameOptionsMiddleware,默认就设置了合适的响应头。

可以通过配置来调整策略:

# settings.py(可选调整)
X_FRAME_OPTIONS = 'DENY'  # 完全禁止(生产环境推荐)
# X_FRAME_OPTIONS = 'SAMEORIGIN'  # 仅允许同源页面内嵌

2. 文件上传安全

文件上传是高危操作,想让上传安全,必须从三个维度验证:

  1. 扩展名白名单
  2. MIME类型检查
  3. 文件内容完整性校验(特别是图片)

下面是一个用 django 表单实现的例子,使用了 python-magicPillow

# myapp/forms.py
from django import forms
from PIL import Image
import magic

class SafeFileUploadForm(forms.Form):
    file = forms.FileField()
    ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif'}
    ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/gif'}

    def clean_file(self):
        file = self.cleaned_data['file']
        
        # 1. 验证扩展名
        ext = '.' + file.name.split('.')[-1].lower()
        if ext not in self.ALLOWED_EXTENSIONS:
            raise forms.ValidationError('不允许的文件类型')
        
        # 2. 验证MIME类型(用python-magic)
        file.seek(0)
        mime = magic.Magic(mime=True)
        mime_type = mime.from_buffer(file.read(1024))
        file.seek(0)
        if mime_type not in self.ALLOWED_MIME_TYPES:
            raise forms.ValidationError('文件类型不匹配')
        
        # 3. 验证图片完整性
        if mime_type.startswith('image/'):
            try:
                img = Image.open(file)
                img.verify()
                file.seek(0)
            except Exception:
                raise forms.ValidationError('图片文件损坏或不安全')
        
        return file

3. 密码安全

密码存储和验证绝不能马虎。django 提供了完善的密码验证器,生产环境务必启用并提高强度:

# settings.py
AUTH_PASSWORD_VALIDATORS = [
    {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
    {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 12}},
    {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
    {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

生产环境安全配置

1. 敏感信息用环境变量管理

敏感数据(密钥、数据库密码)绝对不能硬编码在代码里。推荐使用 python-decouple.env 文件读取:

先安装:

pip install python-decouple

创建 .env 文件(记得加到 .gitignore):

SECRET_KEY=your-production-secret-key-never-expose
DEBUG=False
ALLOWED_HOSTS=yourdomain.com,www.yourdomain.com
DATABASE_URL=postgres://user:pass@localhost/db

settings.py 里加载:

from decouple import config, Csv

SECRET_KEY = config('SECRET_KEY')
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=Csv())

2. 一键部署检查

django 内置了一个检查命令,能发现很多常见的部署安全问题:

python manage.py check --deploy

运行后根据提示修正所有问题,尤其是 SECRET_KEY 未设置或 DEBUG 依然开启这类高风险项。


安全测试与工具

1. 编写基础安全测试用例

把安全要求写成自动化测试,每次改动都能自动验证:

# myapp/tests/test_security.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse

class SecurityTest(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.objects.create_user('test', 'test@test.com', 'StrongPass123!')
    
    def test_csrf_protection(self):
        # 无CSRF令牌的POST应该被拒绝
        response = self.client.post(reverse('submit_comment'), {'text': 'test'})
        self.assertEqual(response.status_code, 403)
    
    def test_xss_protection(self):
        # 输入脚本应该被转义,原本的脚本字符串不会出现在响应页面里
        response = self.client.get(f"{reverse('search')}?q=<script>alert(1)</script>")
        self.assertNotContains(response, '<script>alert(1)</script>')
        self.assertContains(response, '&lt;script&gt;alert(1)&lt;/script&gt;')

2. 常用安全扫描工具

  • Bandit:静态分析 Python 代码中的安全问题

    pip install bandit
    bandit -r .
  • Safety:检查项目依赖中已知的漏洞

    pip install safety
    safety check

定期跑一下这些工具,能帮你及时堵上已知的漏洞。


本章小结

  1. django 已经帮你挡掉了大部分 Web 攻击,但你必须正确配置和使用
  2. 核心理念就两条:输入用白名单输出全转义
  3. 永远不要拼接 SQL,坚持用 ORM 或参数化查询
  4. 生产环境必须关闭 DEBUG,强制使用 HTTPS,并开启各项安全头
  5. 善用自动化工具:check --deploy、Bandit、Safety,定期扫描

安全不是一次性的配置,而是一个持续的工程习惯。

下一步学习


🔗 相关教程推荐

🏷️ 核心标签django安全 XSS防护 CSRF防护 SQL注入 生产环境配置