claude-code/claude-zh/skills/django-security/SKILL.md

593 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 轉義 #}
<script>
var username = {{ username|escapejs }};
</script>
```
### 安全字串處理
```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('<span class="user">{}</span>', 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'] # 受信任的網域
# 範本用法
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">送出</button>
</form>
# 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 與相依套件為最新版本 |
請記住:安全性是一個持續的過程,而非單一產品。請定期審視並更新您的安全實踐。