--- name: django-security description: Django 安全最佳實踐,涵蓋身分驗證、授權、CSRF 保護、SQL 注入預防、XSS 預防以及安全部署配置。 --- # Django 安全最佳實踐 (Django Security Best Practices) 針對 Django 應用程序的綜合安全指南,用於防範常見的漏洞。 ## 何時啟用 - 設置 Django 身分驗證 (Authentication) 與授權 (Authorization)。 - 實作使用者權限與角色。 - 配置生產環境的安全設定。 - 審查 Django 應用程序是否存在安全問題。 - 將 Django 應用程序部署至生產環境。 ## 核心安全設定 ### 生產環境設定配置 (Production Settings Configuration) ```python # settings/production.py import os DEBUG = False # 重要:生產環境絕對不要設定為 True ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',') # 安全標頭 (Security headers) SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_HSTS_SECONDS = 31536000 # 1 年 SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_BROWSER_XSS_FILTER = True X_FRAME_OPTIONS = 'DENY' # HTTPS 與 Cookie SESSION_COOKIE_HTTPONLY = True CSRF_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' CSRF_COOKIE_SAMESITE = 'Lax' # Secret key (必須透過環境變數設定) SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY') if not SECRET_KEY: raise ImproperlyConfigured('必須設定 DJANGO_SECRET_KEY 環境變數') # 密碼驗證規則 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', }, ] ``` ## 身分驗證 (Authentication) ### 自定義使用者模型 (Custom User Model) ```python # apps/users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): """為了提升安全性而實作的自定義使用者模型。""" email = models.EmailField(unique=True) phone = models.CharField(max_length=20, blank=True) USERNAME_FIELD = 'email' # 使用電子郵件作為使用者名稱 REQUIRED_FIELDS = ['username'] class Meta: db_table = 'users' verbose_name = 'User' verbose_name_plural = 'Users' def __str__(self): return self.email # settings/base.py AUTH_USER_MODEL = 'users.User' ``` ### 密碼雜湊 (Password Hashing) ```python # Django 預設使用 PBKDF2。為了更高的安全性,可選用: PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.Argon2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', ] ``` ### 會話管理 (Session Management) ```python # 會話配置 SESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 或 'db' SESSION_CACHE_ALIAS = 'default' SESSION_COOKIE_AGE = 3600 * 24 * 7 # 1 週 SESSION_SAVE_EVERY_REQUEST = False SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 更好的體驗 (UX),但安全性略低 ``` ## 授權 (Authorization) ### 權限 (Permissions) ```python # models.py from django.db import models from django.contrib.auth.models import Permission class Post(models.Model): title = models.CharField(max_length=200) content = models.TextField() author = models.ForeignKey(User, on_delete=models.CASCADE) class Meta: permissions = [ ('can_publish', '可以發佈貼文'), ('can_edit_others', '可以編輯他人的貼文'), ] def user_can_edit(self, user): """檢查使用者是否可以編輯此貼文。""" return self.author == user or user.has_perm('app.can_edit_others') # views.py from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic import UpdateView class PostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): model = Post permission_required = 'app.can_edit_others' raise_exception = True # 回傳 403 錯誤而非重新導向 def get_queryset(self): """僅允許使用者編輯自己的貼文。""" return Post.objects.filter(author=self.request.user) ``` ### 自定義權限 (Custom Permissions) ```python # permissions.py from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): """僅允許擁有者編輯物件。""" def has_object_permission(self, request, view, obj): # 任何請求都允許讀取權限 if request.method in permissions.SAFE_METHODS: return True # 寫入權限僅限擁有者 return obj.author == request.user class IsAdminOrReadOnly(permissions.BasePermission): """允許管理員執行任何操作,其餘使用者僅限讀取。""" def has_permission(self, request, view): if request.method in permissions.SAFE_METHODS: return True return request.user and request.user.is_staff class IsVerifiedUser(permissions.BasePermission): """僅允許已驗證的使用者。""" def has_permission(self, request, view): return request.user and request.user.is_authenticated and request.user.is_verified ``` ### 基於角色的存取控制 (RBAC) ```python # models.py from django.contrib.auth.models import AbstractUser, Group class User(AbstractUser): ROLE_CHOICES = [ ('admin', '管理員'), ('moderator', '版主'), ('user', '一般使用者'), ] role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user') def is_admin(self): return self.role == 'admin' or self.is_superuser def is_moderator(self): return self.role in ['admin', 'moderator'] # Mixins class AdminRequiredMixin: """要求管理員角色的 Mixin。""" def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated or not request.user.is_admin(): from django.core.exceptions import PermissionDenied raise PermissionDenied return super().dispatch(request, *args, **kwargs) ``` ## SQL 注入預防 (SQL Injection Prevention) ### Django ORM 保護機制 ```python # 推薦 (GOOD):Django ORM 會自動處理參數轉義 (Escape) def get_user(username): return User.objects.get(username=username) # 安全 # 推薦 (GOOD):在 raw() 中使用參數化查詢 def search_users(query): return User.objects.raw('SELECT * FROM users WHERE username = %s', [query]) # 錯誤 (BAD):絕對不要直接內插串接使用者輸入 def get_user_bad(username): return User.objects.raw(f'SELECT * FROM users WHERE username = {username}') # 易受攻擊! # 推薦 (GOOD):使用帶有正確轉義的 filter def get_users_by_email(email): return User.objects.filter(email__iexact=email) # 安全 # 推薦 (GOOD):對複雜查詢使用 Q 物件 from django.db.models import Q def search_users_complex(query): return User.objects.filter( Q(username__icontains=query) | Q(email__icontains=query) ) # 安全 ``` ### 關於 raw() 的額外安全性建議 ```python # 若必須使用原始 SQL,請務必使用參數化 User.objects.raw( 'SELECT * FROM users WHERE email = %s AND status = %s', [user_input_email, status] ) ``` ## XSS 預防 ### 範本轉義 (Template Escaping) ```django {# Django 預設會自動轉義變數 - 安全 #} {{ user_input }} {# HTML 將被轉義 #} {# 僅針對受信任的內容顯式標記為安全 #} {{ trusted_html|safe }} {# 不會轉義 #} {# 使用範本過濾器確保 HTML 安全 #} {{ user_input|escape }} {# 同預設行為 #} {{ user_input|striptags }} {# 移除所有 HTML 標籤 #} {# JavaScript 轉義 #} ``` ### 安全字串處理 ```python from django.utils.safestring import mark_safe from django.utils.html import escape # 錯誤 (BAD):在未轉義前絕不要將使用者輸入標記為安全 def render_bad(user_input): return mark_safe(user_input) # 易受攻擊! # 推薦 (GOOD):先轉義,再標記為安全 def render_good(user_input): return mark_safe(escape(user_input)) # 推薦 (GOOD):對包含變數的 HTML 使用 format_html from django.utils.html import format_html def greet_user(username): return format_html('{}', escape(username)) ``` ### HTTP 標頭 ```python # settings.py SECURE_CONTENT_TYPE_NOSNIFF = True # 預防 MIME 嗅探 SECURE_BROWSER_XSS_FILTER = True # 啟用 XSS 過濾器 X_FRAME_OPTIONS = 'DENY' # 預防點擊劫持 (Clickjacking) # 自定義中間件 from django.conf import settings class SecurityHeaderMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) response['X-Content-Type-Options'] = 'nosniff' response['X-Frame-Options'] = 'DENY' response['X-XSS-Protection'] = '1; mode=block' response['Content-Security-Policy'] = "default-src 'self'" return response ``` ## CSRF 保護 (CSRF Protection) ### 預設 CSRF 保護機制 ```python # settings.py - CSRF 預設為啟用 CSRF_COOKIE_SECURE = True # 僅透過 HTTPS 發送 CSRF_COOKIE_HTTPONLY = True # 禁止 JavaScript 存取 CSRF_COOKIE_SAMESITE = 'Lax' # 在某些情況下可預防 CSRF CSRF_TRUSTED_ORIGINS = ['https://example.com'] # 受信任的網域 # 範本用法
# AJAX 請求處理 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; } fetch('/api/endpoint/', { method: 'POST', headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json', }, body: JSON.stringify(data) }); ``` ### 豁免視圖 (View Exemptions - 請謹慎使用) ```python from django.views.decorators.csrf import csrf_exempt @csrf_exempt # 僅在絕對必要時才使用! def webhook_view(request): # 來自外部服務的 Webhook pass ``` ## 檔案上傳安全性 ### 檔案驗證 ```python import os from django.core.exceptions import ValidationError def validate_file_extension(value): """驗證檔案副檔名。""" ext = os.path.splitext(value.name)[1] valid_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.pdf'] if not ext.lower() in valid_extensions: raise ValidationError('不支援的檔案副檔名。') def validate_file_size(value): """驗證檔案大小 (上限 5MB)。""" filesize = value.size if filesize > 5 * 1024 * 1024: raise ValidationError('檔案過大。上限為 5MB。') # models.py class Document(models.Model): file = models.FileField( upload_to='documents/', validators=[validate_file_extension, validate_file_size] ) ``` ### 安全檔案儲存 ```python # settings.py MEDIA_ROOT = '/var/www/media/' MEDIA_URL = '/media/' # 在生產環境使用獨立的媒體網域 MEDIA_DOMAIN = 'https://media.example.com' # 不要直接處理使用者上傳的內容 # 對靜態檔案使用 whitenoise 或 CDN # 對媒體檔案使用獨立伺服器或 S3 ``` ## API 安全性 ### 速率限制 (Rate Limiting) ```python # settings.py REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': [ 'rest_framework.throttling.AnonRateThrottle', 'rest_framework.throttling.UserRateThrottle' ], 'DEFAULT_THROTTLE_RATES': { 'anon': '100/day', 'user': '1000/day', 'upload': '10/hour', } } # 自定義 Throttle 選項 from rest_framework.throttling import UserRateThrottle class BurstRateThrottle(UserRateThrottle): scope = 'burst' rate = '60/min' class SustainedRateThrottle(UserRateThrottle): scope = 'sustained' rate = '1000/day' ``` ### API 身分驗證 ```python # settings.py REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], } # views.py from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated @api_view(['GET', 'POST']) @permission_classes([IsAuthenticated]) def protected_view(request): return Response({'message': '您已通過驗證'}) ``` ## 安全標頭 (Security Headers) ### 內容安全政策 (Content Security Policy) ```python # settings.py CSP_DEFAULT_SRC = "'self'" CSP_SCRIPT_SRC = "'self' https://cdn.example.com" CSP_STYLE_SRC = "'self' 'unsafe-inline'" CSP_IMG_SRC = "'self' data: https:" CSP_CONNECT_SRC = "'self' https://api.example.com" # 中間件實作 class CSPMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) response['Content-Security-Policy'] = ( f"default-src {CSP_DEFAULT_SRC}; " f"script-src {CSP_SCRIPT_SRC}; " f"style-src {CSP_STYLE_SRC}; " f"img-src {CSP_IMG_SRC}; " f"connect-src {CSP_CONNECT_SRC}" ) return response ``` ## 環境變數 (Environment Variables) ### 管理敏感資訊 (Manage Secrets) ```python # 使用 python-decouple 或 django-environ import environ env = environ.Env( # 設定轉型, 預設值 DEBUG=(bool, False) ) # 讀取 .env 檔案 environ.Env.read_env() SECRET_KEY = env('DJANGO_SECRET_KEY') DATABASE_URL = env('DATABASE_URL') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') # .env 檔案 (絕對不要提交此檔案至 Git) DEBUG=False SECRET_KEY=your-secret-key-here DATABASE_URL=postgresql://user:password@localhost:5432/dbname ALLOWED_HOSTS=example.com,www.example.com ``` ## 記錄安全事件 (Logging Security Events) ```python # settings.py LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'file': { 'level': 'WARNING', 'class': 'logging.FileHandler', 'filename': '/var/log/django/security.log', }, 'console': { 'level': 'INFO', 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.security': { 'handlers': ['file', 'console'], 'level': 'WARNING', 'propagate': True, }, 'django.request': { 'handlers': ['file'], 'level': 'ERROR', 'propagate': False, }, }, } ``` ## 快速安全檢核清單 | 檢查項 | 描述 | |-------|-------------| | `DEBUG = False` | 生產環境絕對不可啟用 DEBUG | | 僅限 HTTPS | 強制執行 SSL,使用安全的 Cookie | | 強大且私密的金鑰 | 針對 SECRET_KEY 使用環境變數 | | 密碼驗證規則 | 啟用所有密碼驗證程式 | | CSRF 保護 | 預設為啟用,請勿停用 | | XSS 預防 | Django 會自動轉義,對使用者輸入內容不要隨意使用 `|safe` | | SQL 注入預防 | 使用 ORM,搜尋語句中不可直接串接字串 | | 檔案上傳 | 驗證檔案類型與大小上限 | | 速率限制 | 為 API 端點設定導流 (Throttle) 規則 | | 安全標頭 | 包含 CSP, X-Frame-Options, HSTS 等 | | 記錄日誌 | 記錄所有的安全相關事件 | | 套件更新 | 保持 Django 與相依套件為最新版本 | 請記住:安全性是一個持續的過程,而非單一產品。請定期審視並更新您的安全實踐。