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

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

目录


安全概述

Web安全是开发的核心红线,Django内置了多层防护机制,但需要正确配置才能生效。

OWASP Top 5(Django直接/间接防护)

  • 注入(Injection):ORM防SQL,模板转义防XSS
  • 失效的访问控制:装饰器、权限系统
  • 身份验证失败:认证中间件、密码验证器
  • 安全配置错误:内置check --deploy检查
  • CSRF:原生CSRF中间件

核心安全原则

  1. 最小权限:只给必要权限,避免超级用户操作
  2. 输入白名单,输出全转义:拒绝模糊匹配,输出必须过滤
  3. 深度防御:一层破了还有下一层
  4. 不暴露敏感信息:错误页面、日志要脱敏

XSS防护

XSS(跨站脚本)是最常见的攻击之一,Django默认防住了大部分。

1. 模板自动转义(核心防护)

Django模板引擎默认转义所有HTML特殊字符,不要随便禁用。

错误用法

随意使用|safe过滤器会暴露XSS漏洞:

<!-- 危险!不要这样 -->
<div>{{ user_comment|safe }}</div>
正确用法
<!-- 安全:自动转义 -->
<div>{{ user_comment }}</div>

<!-- 显式转义(可选) -->
<div>{{ user_comment|escape }}</div>

2. 富文本清理(用bleach)

如果必须允许用户输入富文本,用Django生态常用的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头限制资源加载。

基础配置

settings.py的中间件列表开头(或Gunicorn/Nginx配置)添加:

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

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

更严格的CSP可以通过自定义简单中间件或Nginx配置。


CSRF防护

CSRF(跨站请求伪造)利用用户的登录状态发起恶意请求,Django原生中间件防住了99%的情况。

1. 原生中间件(默认开启)

不要删除django.middleware.csrf.CsrfViewMiddleware

2. 模板中使用CSRF令牌

所有表单必须包含{% csrf_token %}

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

3. AJAX请求携带令牌

Axios(推荐):

// 从Cookie获取令牌
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传输
CSRF_COOKIE_SAMESITE = 'Strict'  # 防止跨站请求
CSRF_TRUSTED_ORIGINS = ['https://yourdomain.com']  # 允许的跨域来源

SQL注入防护

SQL注入利用拼接的SQL语句执行恶意查询,Django ORM是核心防护。

1. 禁止拼接SQL

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

2. 正确使用ORM

安全用法
# 安全: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()

其他核心安全防护

1. 点击劫持防护

Django内置XFrameOptionsMiddleware(默认开启),禁止/限制页面在iframe中显示。

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

2. 文件上传安全

文件上传是高危操作,必须三重验证:

  1. 扩展名白名单
  2. MIME类型验证
  3. 图片完整性检查(如果是图片)

用Django表单实现:

# 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)

不要把敏感信息写在代码里,用python-decouple.env读取:

pip install python-decouple

.env文件(不要提交到Git):

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. 生产环境一键配置(用check --deploy)

运行Django内置的部署安全检查:

python manage.py check --deploy

根据提示修正所有问题。


安全测试与工具

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应该返回403
        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, '<script>alert(1)</script>')

2. 常用安全工具

  • Bandit:静态代码安全扫描
    pip install bandit
    bandit -r .
  • Safety:检查依赖包漏洞
    pip install safety
    safety check

本章小结

  1. Django默认防住了大部分漏洞,但需要正确配置
  2. 输入白名单,输出全转义是核心
  3. 禁止SQL拼接,必须用ORM或参数化查询
  4. 生产环境必须关闭DEBUG,启用HTTPS
  5. 定期用安全工具扫描,运行check --deploy

下一步学习


🔗 相关教程推荐

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