django.moscow
УслугиЦеныПроцессОбучениеБлог
Связаться

django.moscow

Поддержка и развитие Django-сайтов с гарантиями. 15+ лет опыта в веб-разработке.

Услуги

  • Аудит и стабилизация
  • Поддержка и SLA
  • Оптимизация
  • Развитие и фичи

Информация

  • Цены
  • Процесс работы
  • Блог
  • Контакты
  • Реквизиты

Контакты

  • constantin@potapov.me
  • @potapov_me
Политика конфиденциальности•Согласие на обработку ПД

© 2025 django.moscow. Все права защищены.

Сделано с ❤️ в России • potapov.me

\n```\n\n4. Браузер автоматически отправляет cookies (сессию)\n5. **Деньги переведены без ведома пользователя!**\n\n### Django защита\n\n```python\n# settings.py - CSRF middleware включён по умолчанию\nMIDDLEWARE = [\n # ...\n 'django.middleware.csrf.CsrfViewMiddleware', # ← Обязательно!\n # ...\n]\n```\n\n### В шаблонах\n\n```django\n
\n {% csrf_token %} {# ← ОБЯЗАТЕЛЬНО в каждой POST форме #}\n \n \n
\n```\n\n### В AJAX запросах\n\n```javascript\n// Получить CSRF token из cookie\nfunction getCookie(name) {\n let cookieValue = null;\n if (document.cookie && document.cookie !== '') {\n const cookies = document.cookie.split(';');\n for (let i = 0; i < cookies.length; i++) {\n const cookie = cookies[i].trim();\n if (cookie.substring(0, name.length + 1) === (name + '=')) {\n cookieValue = decodeURIComponent(cookie.substring(name.length + 1));\n break;\n }\n }\n }\n return cookieValue;\n}\n\nconst csrftoken = getCookie('csrftoken');\n\n// Fetch API\nfetch('/api/endpoint/', {\n method: 'POST',\n headers: {\n 'X-CSRFToken': csrftoken,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(data)\n})\n\n// Или установить глобально для всех запросов\n// (если используете axios или другую библиотеку)\naxios.defaults.headers.common['X-CSRFToken'] = csrftoken;\n```\n\n### Когда можно отключить CSRF\n\n```python\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.utils.decorators import method_decorator\n\n# ❌ ОПАСНО! Используйте только для:\n# 1. Публичных API без авторизации\n# 2. Webhook endpoints (с другой аутентификацией)\n\n@csrf_exempt\ndef webhook_receiver(request):\n # ОБЯЗАТЕЛЬНО проверить подпись webhook!\n signature = request.headers.get('X-Hub-Signature')\n if not verify_signature(request.body, signature):\n return HttpResponseForbidden()\n # ...\n\n# Для class-based views\n@method_decorator(csrf_exempt, name='dispatch')\nclass WebhookView(View):\n def post(self, request):\n # ...\n```\n\n### Дополнительные настройки CSRF\n\n```python\n# settings.py\n\n# Доверенные origins для CSRF (для CORS requests)\nCSRF_TRUSTED_ORIGINS = [\n 'https://yourdomain.com',\n 'https://*.yourdomain.com',\n]\n\n# Кастомный header name\nCSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'\n\n# Cookie settings\nCSRF_COOKIE_NAME = 'csrftoken'\nCSRF_COOKIE_AGE = 31449600 # 1 год\nCSRF_COOKIE_DOMAIN = '.yourdomain.com' # Для поддоменов\nCSRF_COOKIE_PATH = '/'\nCSRF_COOKIE_SECURE = True # HTTPS only\nCSRF_COOKIE_HTTPONLY = True # Защита от XSS\nCSRF_COOKIE_SAMESITE = 'Lax' # Strict или Lax\n```\n\n**Результат**: Защита от подделки запросов со сторонних сайтов.\n\n---\n\n## 5. SQL Injection — почему ORM не панацея\n\n### Безопасно: ORM\n\n```python\n# ✅ БЕЗОПАСНО - параметризованные запросы\nusername = request.GET.get('username')\nusers = User.objects.filter(username=username)\n\n# ✅ БЕЗОПАСНО - Q objects\nfrom django.db.models import Q\nUser.objects.filter(\n Q(username=username) | Q(email=email)\n)\n\n# ✅ БЕЗОПАСНО - даже со строками\nUser.objects.filter(username__contains=user_input)\n```\n\n**SQL под капотом:**\n```sql\nSELECT * FROM auth_user\nWHERE username = %s -- Параметр экранирован!\n```\n\n### Опасно: Raw SQL\n\n```python\n# ❌ ОПАСНО! SQL Injection\nusername = request.GET.get('username')\nUser.objects.raw(\n f\"SELECT * FROM auth_user WHERE username = '{username}'\"\n)\n\n# Атака: ?username=' OR '1'='1\n# Результат: SELECT * FROM auth_user WHERE username = '' OR '1'='1'\n# → Вернёт ВСЕХ пользователей!\n```\n\n### Правильное использование Raw SQL\n\n```python\n# ✅ БЕЗОПАСНО - параметризованный запрос\nUser.objects.raw(\n \"SELECT * FROM auth_user WHERE username = %s\",\n [username] # ← Параметр экранируется\n)\n\n# ✅ БЕЗОПАСНО - с cursor\nfrom django.db import connection\n\nwith connection.cursor() as cursor:\n cursor.execute(\n \"SELECT * FROM auth_user WHERE username = %s AND active = %s\",\n [username, True]\n )\n results = cursor.fetchall()\n```\n\n### Опасность в extra() и raw()\n\n```python\n# ❌ ОПАСНО!\nUser.objects.extra(\n where=[f\"username = '{user_input}'\"]\n)\n\n# ✅ БЕЗОПАСНО\nUser.objects.extra(\n where=[\"username = %s\"],\n params=[user_input]\n)\n```\n\n### NoSQL Injection (MongoDB, etc.)\n\n```python\n# Даже с NoSQL ORM нужна осторожность\n# ❌ ОПАСНО (если используете djongo или mongoengine)\nfilter_query = json.loads(request.body) # Пользовательский ввод!\nDocument.objects.filter(**filter_query) # Может содержать $where, $ne, etc.\n\n# ✅ БЕЗОПАСНО - валидация структуры\nallowed_fields = ['username', 'email', 'age']\nfilter_query = {\n k: v for k, v in user_input.items()\n if k in allowed_fields\n}\n```\n\n**Результат**: БД защищена от injection атак.\n\n---\n\n## 6. XSS (Cross-Site Scripting)\n\n### Типы XSS\n\n| Тип | Описание | Пример |\n|-----|----------|--------|\n| Reflected | Вредоносный код в URL/параметрах | `?q=` |\n| Stored | Код сохранён в БД | Комментарий с `\" #}\n{# Результат:

Hello, <script>alert('XSS')</script>!

#}\n```\n\n### Когда отключается экранирование\n\n```django\n{# ❌ ОПАСНО! |safe отключает экранирование #}\n{{ user_comment|safe }}\n\n{# ❌ ОПАСНО! #}\n{% autoescape off %}\n {{ user_input }}\n{% endautoescape %}\n\n{# ✅ Используйте safe только для доверенного контента #}\n{# Например, markdown после санитизации: #}\n{{ content|markdown|safe }}\n```\n\n### Санитизация HTML\n\n```python\n# Установка\npip install bleach\n\n# Использование\nimport bleach\n\nALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'a']\nALLOWED_ATTRS = {'a': ['href', 'title']}\n\ndef sanitize_html(dirty_html):\n return bleach.clean(\n dirty_html,\n tags=ALLOWED_TAGS,\n attributes=ALLOWED_ATTRS,\n strip=True # Удалить запрещённые теги\n )\n\n# В view\nclean_comment = sanitize_html(request.POST.get('comment'))\ncomment.content = clean_comment\ncomment.save()\n```\n\n### JSON и JavaScript context\n\n```django\n{# ❌ ОПАСНО! #}\n\n\n\n{# ✅ БЕЗОПАСНО - используйте json_script #}\n{{ user_data|json_script:\"user-data\" }}\n\n```\n\n### Content Security Policy (CSP)\n\n```python\n# pip install django-csp\n\n# settings.py\nMIDDLEWARE = [\n # ...\n 'csp.middleware.CSPMiddleware',\n]\n\n# CSP политика\nCSP_DEFAULT_SRC = (\"'self'\",)\nCSP_SCRIPT_SRC = (\n \"'self'\",\n 'https://cdn.jsdelivr.net', # Trusted CDN\n)\nCSP_STYLE_SRC = (\n \"'self'\",\n \"'unsafe-inline'\", # Только если необходимо\n)\nCSP_IMG_SRC = (\"'self'\", 'data:', 'https:')\nCSP_FONT_SRC = (\"'self'\", 'https://fonts.gstatic.com')\nCSP_CONNECT_SRC = (\"'self'\", 'https://api.yourdomain.com')\n\n# Reporting\nCSP_REPORT_URI = '/csp-report/'\nCSP_REPORT_ONLY = False # True для тестирования\n\n# views.py - CSP report endpoint\n@csrf_exempt\ndef csp_report(request):\n if request.method == 'POST':\n logger.warning('CSP Violation: %s', request.body)\n return HttpResponse()\n```\n\n**Результат**: XSS атаки блокируются на всех уровнях.\n\n---\n\n## 7. Аутентификация и Session Security\n\n### Password Hashing\n\n```python\n# settings.py\nPASSWORD_HASHERS = [\n 'django.contrib.auth.hashers.Argon2PasswordHasher', # Самый стойкий\n 'django.contrib.auth.hashers.PBKDF2PasswordHasher', # По умолчанию\n 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',\n 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',\n]\n\n# Для Argon2\n# pip install django[argon2]\n```\n\n### Password Validation\n\n```python\n# settings.py\nAUTH_PASSWORD_VALIDATORS = [\n {\n 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',\n },\n {\n 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',\n 'OPTIONS': {\n 'min_length': 12, # Минимум 12 символов\n }\n },\n {\n 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',\n },\n {\n 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',\n },\n # Кастомный валидатор\n {\n 'NAME': 'myapp.validators.PasswordComplexityValidator',\n },\n]\n\n# myapp/validators.py\nimport re\nfrom django.core.exceptions import ValidationError\n\nclass PasswordComplexityValidator:\n def validate(self, password, user=None):\n if not re.search(r'[A-Z]', password):\n raise ValidationError(\"Password must contain uppercase letter\")\n if not re.search(r'[a-z]', password):\n raise ValidationError(\"Password must contain lowercase letter\")\n if not re.search(r'[0-9]', password):\n raise ValidationError(\"Password must contain digit\")\n if not re.search(r'[!@#$%^&*(),.?\":{}|<>]', password):\n raise ValidationError(\"Password must contain special character\")\n\n def get_help_text(self):\n return \"Password must contain uppercase, lowercase, digit, and special character\"\n```\n\n### Brute-Force Protection\n\n```python\n# pip install django-axes\n\n# settings.py\nINSTALLED_APPS = [\n # ...\n 'axes',\n]\n\nMIDDLEWARE = [\n # AxesMiddleware должен быть ПОСЛЕ AuthenticationMiddleware\n 'django.contrib.auth.middleware.AuthenticationMiddleware',\n 'axes.middleware.AxesMiddleware',\n]\n\nAUTHENTICATION_BACKENDS = [\n 'axes.backends.AxesStandaloneBackend', # AxesBackend первым!\n 'django.contrib.auth.backends.ModelBackend',\n]\n\n# Настройки Axes\nAXES_FAILURE_LIMIT = 5 # Блокировка после 5 неудачных попыток\nAXES_COOLOFF_TIME = 1 # Час блокировки\nAXES_LOCKOUT_MESSAGE = 'Too many failed attempts. Try again later.'\nAXES_RESET_ON_SUCCESS = True # Сброс счётчика после успешного входа\nAXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True # По user + IP\n\n# Whitelist IP\nAXES_IP_WHITELIST = ['127.0.0.1']\nAXES_NEVER_LOCKOUT_WHITELIST = True\n\n# Blacklist\nAXES_IP_BLACKLIST = []\nAXES_ONLY_USER_FAILURES = False # False = по IP тоже блокирует\n```\n\n### Two-Factor Authentication (2FA)\n\n```python\n# pip install django-otp qrcode\n\n# settings.py\nINSTALLED_APPS = [\n # ...\n 'django_otp',\n 'django_otp.plugins.otp_totp',\n]\n\nMIDDLEWARE = [\n # ...\n 'django_otp.middleware.OTPMiddleware',\n]\n\n# views.py\nfrom django_otp.decorators import otp_required\n\n@otp_required\ndef sensitive_view(request):\n # Требует 2FA\n pass\n\n# Для админки\nfrom django_otp.admin import OTPAdminSite\nadmin.site.__class__ = OTPAdminSite\n```\n\n### Session Security\n\n```python\n# settings.py\n\n# Session настройки\nSESSION_COOKIE_AGE = 1209600 # 2 недели\nSESSION_SAVE_EVERY_REQUEST = False # True = обновлять при каждом запросе\nSESSION_EXPIRE_AT_BROWSER_CLOSE = False # True = сессия только на время браузера\n\n# Security\nSESSION_COOKIE_HTTPONLY = True # Защита от XSS\nSESSION_COOKIE_SECURE = True # HTTPS only\nSESSION_COOKIE_SAMESITE = 'Lax' # Strict, Lax, или None\n\n# Session backend (Redis для production)\nSESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'\n\n# Или полностью в Redis\n# pip install django-redis\nSESSION_ENGINE = 'django.contrib.sessions.backends.cache'\nSESSION_CACHE_ALIAS = 'default'\n```\n\n**Результат**: Многоуровневая защита учётных записей.\n\n---\n\n## 8. File Upload Security\n\n### Опасности загрузки файлов\n\n1. **Remote Code Execution**: Загрузка `.php`, `.py`, `.jsp` на сервер\n2. **Path Traversal**: `../../etc/passwd` в имени файла\n3. **XSS**: Загрузка HTML/SVG с JavaScript\n4. **DoS**: Огромные файлы\n5. **Malware**: Вирусы в файлах\n\n### Валидация типов файлов\n\n```python\n# models.py\nfrom django.core.exceptions import ValidationError\nimport os\nimport magic # python-magic\n\nALLOWED_EXTENSIONS = ['.pdf', '.jpg', '.jpeg', '.png', '.doc', '.docx']\nALLOWED_MIMETYPES = [\n 'application/pdf',\n 'image/jpeg',\n 'image/png',\n 'application/msword',\n]\n\ndef validate_file_extension(value):\n ext = os.path.splitext(value.name)[1].lower()\n if ext not in ALLOWED_EXTENSIONS:\n raise ValidationError(\n f'Unsupported file extension. Allowed: {\", \".join(ALLOWED_EXTENSIONS)}'\n )\n\ndef validate_file_mimetype(value):\n # ВНИМАНИЕ: Не доверяйте только расширению!\n # Проверяйте реальный MIME type\n file_mime = magic.from_buffer(value.read(1024), mime=True)\n value.seek(0) # Вернуть указатель в начало\n\n if file_mime not in ALLOWED_MIMETYPES:\n raise ValidationError(f'Invalid file type: {file_mime}')\n\ndef validate_file_size(value):\n filesize = value.size\n if filesize > 5 * 1024 * 1024: # 5MB\n raise ValidationError('Max file size is 5MB')\n\nclass Document(models.Model):\n upload = models.FileField(\n upload_to='documents/%Y/%m/%d/',\n validators=[\n validate_file_extension,\n validate_file_mimetype,\n validate_file_size,\n ]\n )\n```\n\n### Безопасное имя файла\n\n```python\n# Никогда не используйте оригинальное имя напрямую!\nimport uuid\nfrom django.utils.text import slugify\n\ndef secure_filename(filename):\n # Удалить путь\n filename = os.path.basename(filename)\n # Slugify имени\n name, ext = os.path.splitext(filename)\n name = slugify(name)\n # Добавить UUID для уникальности\n return f\"{name}-{uuid.uuid4().hex[:8]}{ext}\"\n\n# В модели\ndef upload_path(instance, filename):\n return f'uploads/{instance.user.id}/{secure_filename(filename)}'\n\nclass Document(models.Model):\n file = models.FileField(upload_to=upload_path)\n```\n\n### Ограничения на уровне Django\n\n```python\n# settings.py\n\n# Максимальный размер загружаемых данных\nDATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB в памяти\nFILE_UPLOAD_MAX_MEMORY_SIZE = 5242880 # 5MB в памяти\n\n# Временные файлы\nFILE_UPLOAD_TEMP_DIR = '/tmp/django-uploads'\nFILE_UPLOAD_PERMISSIONS = 0o644\n\n# Хранение файлов вне MEDIA_ROOT для исполняемых\nPRIVATE_MEDIA_ROOT = '/var/app/private-media/'\n```\n\n### Сканирование на вирусы\n\n```python\n# pip install pyclamd\n\nimport pyclamd\n\ndef scan_uploaded_file(file):\n cd = pyclamd.ClamdUnixSocket()\n\n # Проверить что ClamAV доступен\n if not cd.ping():\n raise Exception('ClamAV is not running')\n\n # Сканировать файл\n scan_result = cd.scan_stream(file.read())\n file.seek(0)\n\n if scan_result:\n raise ValidationError('File contains malware!')\n\n# В view или form\ndef clean_file(self):\n file = self.cleaned_data.get('file')\n scan_uploaded_file(file)\n return file\n```\n\n### CDN и изоляция\n\n```python\n# settings.py - Хранить загрузки отдельно\nAWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'\nMEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'\n\n# Отдельный bucket для пользовательских загрузок\nUSER_UPLOADS_BUCKET = 'user-uploads-isolated'\n\n# Content-Disposition для скачивания вместо отображения\nAWS_S3_OBJECT_PARAMETERS = {\n 'ContentDisposition': 'attachment', # Принудительное скачивание\n}\n```\n\n**Результат**: Загрузки файлов не создают уязвимостей.\n\n---\n\n## 9. Security Headers\n\n```python\n# settings.py\n\n# X-Frame-Options (защита от clickjacking)\nX_FRAME_OPTIONS = 'DENY' # Или 'SAMEORIGIN'\n\n# X-Content-Type-Options\nSECURE_CONTENT_TYPE_NOSNIFF = True\n\n# X-XSS-Protection (legacy, но всё ещё полезен)\nSECURE_BROWSER_XSS_FILTER = True\n\n# Referrer-Policy\nSECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'\n# Опции: no-referrer, origin, strict-origin, etc.\n\n# Permissions-Policy (ранее Feature-Policy)\nPERMISSIONS_POLICY = {\n \"geolocation\": [], # Отключить для всех\n \"camera\": [],\n \"microphone\": [],\n \"payment\": [\"self\"], # Только для своего origin\n}\n```\n\n### Кастомный middleware для headers\n\n```python\n# middleware.py\nclass SecurityHeadersMiddleware:\n def __init__(self, get_response):\n self.get_response = get_response\n\n def __call__(self, request):\n response = self.get_response(request)\n\n # Permissions-Policy\n response['Permissions-Policy'] = 'geolocation=(), camera=(), microphone=()'\n\n # Expect-CT (Certificate Transparency)\n response['Expect-CT'] = 'max-age=86400, enforce'\n\n # NEL (Network Error Logging)\n response['NEL'] = '{\"report_to\":\"default\",\"max_age\":31536000}'\n\n return response\n\n# settings.py\nMIDDLEWARE = [\n 'myapp.middleware.SecurityHeadersMiddleware',\n # ...\n]\n```\n\n**Результат**: Браузеры применяют дополнительные защитные механизмы.\n\n---\n\n## 10. Dependencies Security\n\n### Регулярная проверка уязвимостей\n\n```bash\n# pip-audit (официальный инструмент от PyPA)\npip install pip-audit\npip-audit\n\n# Safety\npip install safety\nsafety check\n\n# С GitHub Advisory Database\nsafety check --json\n\n# Snyk (более продвинутый)\nnpm install -g snyk\nsnyk test --file=requirements.txt\n```\n\n### Автоматизация в CI/CD\n\n```yaml\n# .github/workflows/security.yml\nname: Security Check\n\non: [push, pull_request]\n\njobs:\n security:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v3\n\n - name: Set up Python\n uses: actions/setup-python@v4\n with:\n python-version: '3.11'\n\n - name: Install dependencies\n run: |\n pip install pip-audit safety\n\n - name: Run pip-audit\n run: pip-audit --requirement requirements.txt\n\n - name: Run safety check\n run: safety check --file requirements.txt --json\n\n - name: Run bandit (code security)\n run: |\n pip install bandit\n bandit -r . -f json -o bandit-report.json\n```\n\n### Dependabot configuration\n\n```yaml\n# .github/dependabot.yml\nversion: 2\nupdates:\n - package-ecosystem: \"pip\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n open-pull-requests-limit: 10\n reviewers:\n - \"security-team\"\n labels:\n - \"security\"\n - \"dependencies\"\n```\n\n**Результат**: Уязвимости в зависимостях детектируются автоматически.\n\n---\n\n## Финальный Production Checklist\n\n### Критически важно (нельзя игнорировать)\n\n- ✅ `DEBUG = False`\n- ✅ `SECRET_KEY` из environment variables\n- ✅ `ALLOWED_HOSTS` настроен корректно\n- ✅ HTTPS настроен с валидным сертификатом\n- ✅ `SESSION_COOKIE_SECURE = True`\n- ✅ `CSRF_COOKIE_SECURE = True`\n- ✅ `SECURE_SSL_REDIRECT = True`\n- ✅ `python manage.py check --deploy` без ошибок\n- ✅ Все credentials в `.env` (не в коде!)\n- ✅ Database user с минимальными привилегиями\n\n### Обязательно (сильно рекомендуется)\n\n- ✅ HSTS настроен (`SECURE_HSTS_SECONDS`)\n- ✅ CSP (Content Security Policy) активен\n- ✅ File upload validation\n- ✅ Rate limiting (django-axes или django-ratelimit)\n- ✅ Logging настроен корректно\n- ✅ Error monitoring (Sentry)\n- ✅ Regular backups (автоматические)\n- ✅ pip-audit в CI/CD pipeline\n\n### Настоятельно рекомендуется\n\n- ✅ WAF (Web Application Firewall) - CloudFlare, AWS WAF\n- ✅ 2FA для администраторов\n- ✅ Penetration testing (ежегодно)\n- ✅ Security code review process\n- ✅ Secrets scanning в Git (git-secrets, truffleHog)\n- ✅ GDPR/CCPA compliance (если applicable)\n- ✅ Security.txt (`/.well-known/security.txt`)\n- ✅ Bug bounty program (для крупных проектов)\n\n---\n\n## Автоматизация проверок\n\n### Django check --deploy\n\n```bash\n# Запустить все security проверки\npython manage.py check --deploy\n\n# Пример вывода предупреждений:\n# WARNINGS:\n# ?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting.\n# ?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True.\n# ?: (security.W012) SESSION_COOKIE_SECURE is not set to True.\n```\n\n### Pre-commit hooks\n\n```bash\n# .pre-commit-config.yaml\nrepos:\n - repo: https://github.com/pycqa/bandit\n rev: '1.7.5'\n hooks:\n - id: bandit\n args: ['-r', 'myapp/', '-f', 'screen']\n\n - repo: https://github.com/PyCQA/flake8\n rev: '6.1.0'\n hooks:\n - id: flake8\n additional_dependencies: [flake8-bugbear, flake8-security]\n\n# Установка\npip install pre-commit\npre-commit install\n```\n\n---\n\n## Инструменты и ресурсы\n\n### Обязательные инструменты\n\n- **django-axes** — Brute-force protection\n- **django-csp** — Content Security Policy\n- **pip-audit** — Vulnerability scanning\n- **bandit** — Code security linter\n- **safety** — Dependency checker\n\n### Сканеры и тестирование\n\n- **OWASP ZAP** — Automated security testing\n- **Burp Suite** — Professional security testing\n- **Nikto** — Web server scanner\n- **SQLMap** — SQL injection testing\n\n### Мониторинг\n\n- **Sentry** — Error & security monitoring\n- **DataDog** — APM & security\n- **Wazuh** — Security monitoring & threat detection\n\n### Образовательные ресурсы\n\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [Django Security Releases](https://www.djangoproject.com/weblog/)\n- [PortSwigger Web Security Academy](https://portswigger.net/web-security)\n- [Mozilla Web Security](https://infosec.mozilla.org/guidelines/web_security)\n\n---\n\n## Compliance и Regulations\n\n### GDPR (EU)\n\n```python\n# Право на забвение (Right to be forgotten)\nclass User(AbstractUser):\n def anonymize(self):\n \"\"\"GDPR compliant user anonymization\"\"\"\n self.email = f\"deleted_{self.id}@example.com\"\n self.first_name = \"Deleted\"\n self.last_name = \"User\"\n self.is_active = False\n self.save()\n\n # Удалить или анонимизировать связанные данные\n self.profile.delete()\n self.comments.all().update(content=\"[deleted]\")\n\n# Data export\ndef export_user_data(user):\n \"\"\"GDPR compliant data export\"\"\"\n return {\n 'profile': UserSerializer(user).data,\n 'comments': CommentSerializer(user.comments.all(), many=True).data,\n 'orders': OrderSerializer(user.orders.all(), many=True).data,\n }\n```\n\n### PCI DSS (Payment Card Industry)\n\n```python\n# НИКОГДА не храните:\n# - Полный номер карты (только last 4 digits)\n# - CVV/CVC\n# - PIN\n\nclass Payment(models.Model):\n # ✅ Только последние 4 цифры\n card_last4 = models.CharField(max_length=4)\n\n # ✅ Токен от payment gateway (Stripe, etc.)\n payment_token = models.CharField(max_length=255)\n\n # ❌ НИКОГДА так не делайте!\n # card_number = models.CharField(max_length=16) # ЗАПРЕЩЕНО!\n # cvv = models.CharField(max_length=3) # ЗАПРЕЩЕНО!\n```\n\n---\n\n## Заключение\n\nБезопасность Django-приложения — это не чек-лист, который можно пройти один раз. Это **непрерывный процесс**:\n\n1. **Проектирование**: Security by design\n2. **Разработка**: Secure coding practices\n3. **Тестирование**: Automated security tests\n4. **Деплой**: Hardened configuration\n5. **Мониторинг**: Real-time threat detection\n6. **Обновление**: Regular patches and updates\n\n**Ключевые принципы:**\n\n- **Defense in Depth**: Многоуровневая защита\n- **Least Privilege**: Минимальные права доступа\n- **Fail Securely**: Безопасный режим при ошибках\n- **Don't Trust Input**: Валидация всех данных\n- **Keep it Simple**: Простота = безопасность\n\n**Помните**:\n> \"Безопасность — это цепь. Она настолько сильна, насколько сильно её самое слабое звено.\"\n\nНачните с `python manage.py check --deploy` и исправьте все предупреждения. Остальное приложится!\n","timeRequired":"34 мин","inLanguage":"ru-RU","articleSection":"Безопасность"}
  1. /
  2. Блог
  3. /
  4. Чеклист безопасности Django перед продакшеном
Безопасность
10 декабря 2025
34 мин

Чеклист безопасности Django перед продакшеном

Чеклист безопасности Django перед продакшеном

TL;DR

Безопасность Django-приложения — это многоуровневая защита. Ключевые моменты:

  • Django защищает из коробки: XSS, CSRF, SQL Injection, Clickjacking
  • Но нужна правильная настройка: DEBUG=False, HTTPS, SECRET_KEY из env
  • OWASP Top 10 coverage: Все критичные уязвимости покрыты при правильной конфигурации
  • Регулярные обновления: 80% уязвимостей — в зависимостях
  • Мониторинг обязателен: Sentry + django-axes для детекции атак

Результат: Защищенное приложение, готовое к audit и compliance проверкам.


Введение

Django имеет отличную репутацию в области безопасности благодаря встроенной защите от большинства OWASP Top 10 уязвимостей. Однако, неправильная конфигурация может свести все это на нет.

Статистика:

95% успешных атак на Django-приложения происходят из-за misconfiguration, а не из-за уязвимостей в самом фреймворке. — OWASP Report 2023

В этой статье — полный security checklist с примерами реальных атак и методами защиты.


OWASP Top 10 в контексте Django

OWASP УязвимостьDjango защитаНужны действия
A01 Broken Access Control❌ Нет✅ Permissions, декораторы
A02 Cryptographic Failures✅ PBKDF2/Argon2⚠️ SECRET_KEY, HTTPS
A03 Injection✅ ORM escaping⚠️ Не использовать raw SQL
A04 Insecure Design❌ Архитектура✅ Security by design
A05 Security Misconfiguration❌ Нет✅ DEBUG=False, check --deploy
A06 Vulnerable Components❌ Нет✅ pip-audit, safety
A07 Auth & Session⚠️ Partial✅ Rate limiting, 2FA
A08 Data Integrity⚠️ CSRF✅ Signatures, хэширование
A09 Logging Failures❌ Нет✅ Настроить logging
A10 SSRF❌ Нет✅ Валидация URL, whitelist

1. SECRET_KEY — основа криптографии Django

Что это и зачем

SECRET_KEY используется для:

  • Подписи сессий (session cookies)
  • CSRF токенов
  • Подписи данных (signing module)
  • Password reset tokens

Если SECRET_KEY утекёт → атакующий может:

  • Подделать сессии любого пользователя
  • Обойти CSRF защиту
  • Создать валидные password reset ссылки

Правильная настройка

# ❌ ОПАСНО! Никогда так не делайте
SECRET_KEY = 'django-insecure-hardcoded-key-123456'

# ❌ ОПАСНО! В коде
SECRET_KEY = '7d@kw!_x9%m&amp;2v8n#p$q^r*s+t=u'

# ✅ ПРАВИЛЬНО: Из переменных окружения
import os
from django.core.exceptions import ImproperlyConfigured

SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')

if not SECRET_KEY:
    raise ImproperlyConfigured(
        "DJANGO_SECRET_KEY environment variable must be set"
    )

# ✅ Или с django-environ
import environ
env = environ.Env()
SECRET_KEY = env('SECRET_KEY')  # Raises error if not set

Генерация нового ключа

# Способ 1: Django utility
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'

# Способ 2: OpenSSL
openssl rand -base64 50

# Способ 3: Python secrets module
python -c 'import secrets; print(secrets.token_urlsafe(50))'

Ротация SECRET_KEY

Если ключ скомпрометирован:

# settings.py
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Старый ключ для валидации существующих токенов
SECRET_KEY_FALLBACKS = [
    os.environ.get('DJANGO_SECRET_KEY_OLD'),
]

# После ротации все сессии будут инвалидированы
# Пользователи должны войти заново

⚠️ Важно: После смены SECRET_KEY:

  • Все сессии станут невалидными
  • CSRF токены перестанут работать
  • Password reset ссылки станут недействительны

Результат: Основа для всех cryptographic операций защищена.


2. DEBUG = False в Production

Почему DEBUG=True опасно

При DEBUG=True Django показывает:

  1. Детальные tracebacks с:

    • Структурой БД (таблицы, колонки)
    • Путями к файлам на сервере
    • Значениями переменных (могут быть пароли!)
    • Версиями библиотек
  2. Настройки проекта (settings.py)

  3. SQL-запросы с данными

  4. Стек вызовов с логикой приложения

Реальный пример утечки

# views.py
def user_profile(request, user_id):
    api_key = "sk-proj-1234567890"  # API ключ в коде
    user = User.objects.get(id=user_id)
    # ...

При ошибке с DEBUG=True:

Local vars:
    api_key = 'sk-proj-1234567890'  # ← УТЕКАЕТ!
    user_id = 42

Правильная настройка

# settings.py
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'

# Альтернатива: явный production режим
ENVIRONMENT = os.environ.get('ENVIRONMENT', 'production')
DEBUG = ENVIRONMENT == 'development'

# ОБЯЗАТЕЛЬНО при DEBUG=False
ALLOWED_HOSTS = [
    'yourdomain.com',
    'www.yourdomain.com',
    '.yourdomain.com',  # Все поддомены
]

# Для production
if not DEBUG:
    # Обязательно настройте error handling
    ADMINS = [('Admin Name', 'admin@yourdomain.com')]
    SERVER_EMAIL = 'error@yourdomain.com'

    # Custom error pages
    TEMPLATES[0]['OPTIONS']['debug'] = False

Custom error pages

# urls.py
handler400 = 'myapp.views.bad_request'
handler403 = 'myapp.views.permission_denied'
handler404 = 'myapp.views.page_not_found'
handler500 = 'myapp.views.server_error'

# views.py
from django.shortcuts import render

def page_not_found(request, exception):
    return render(request, '404.html', status=404)

def server_error(request):
    return render(request, '500.html', status=500)

Результат: Атакующий не получает информацию о структуре приложения.


3. HTTPS Everywhere

Почему HTTPS обязателен

Без HTTPS атакующий может:

  • Перехватить сессионные cookies (session hijacking)
  • Прочитать передаваемые данные (пароли, личная информация)
  • Модифицировать ответы сервера (man-in-the-middle)

Полная конфигурация HTTPS

# settings.py (только для production!)
if not DEBUG:
    # 1. Редирект всех HTTP → HTTPS
    SECURE_SSL_REDIRECT = True

    # 2. Для проксирующих серверов (nginx, CloudFlare, AWS ALB)
    SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

    # 3. Secure cookies (HTTPS only)
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True

    # 4. HSTS (HTTP Strict Transport Security)
    SECURE_HSTS_SECONDS = 31536000  # 1 год
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_HSTS_PRELOAD = True  # Для включения в HSTS preload list

    # 5. Дополнительные настройки cookies
    SESSION_COOKIE_HTTPONLY = True  # Защита от XSS
    CSRF_COOKIE_HTTPONLY = True
    SESSION_COOKIE_SAMESITE = 'Lax'  # Защита от CSRF
    CSRF_COOKIE_SAMESITE = 'Lax'

HSTS Preload

Что это: Браузеры предзагружают список сайтов, которые ВСЕГДА должны открываться через HTTPS.

Как добавить сайт:

  1. Настройте SECURE_HSTS_PRELOAD = True
  2. Убедитесь, что HSTS работает минимум 18 недель
  3. Подайте заявку: https://hstspreload.org/

⚠️ Важно: Удаление из preload list может занять месяцы!

Let's Encrypt для бесплатного SSL

# Установка Certbot
sudo apt install certbot python3-certbot-nginx

# Автоматическая настройка для nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Автопродление (добавится в cron)
sudo certbot renew --dry-run

Проверка конфигурации

# Django check
python manage.py check --deploy

# SSL Labs test (A+ rating)
# https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com

# Mozilla Observatory
# https://observatory.mozilla.org/

Результат: Все данные передаются в зашифрованном виде.


4. CSRF Protection

Как работает CSRF атака

Сценарий:

  1. Пользователь авторизован на bank.com
  2. Открывает злонамеренный сайт evil.com
  3. evil.com содержит:
<form action="https://bank.com/transfer" method="POST">
  <input name="to" value="attacker_account">
  <input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
  1. Браузер автоматически отправляет cookies (сессию)
  2. Деньги переведены без ведома пользователя!

Django защита

# settings.py - CSRF middleware включён по умолчанию
MIDDLEWARE = [
    # ...
    'django.middleware.csrf.CsrfViewMiddleware',  # ← Обязательно!
    # ...
]

В шаблонах

<form method="post">
    {% csrf_token %}  {# ← ОБЯЗАТЕЛЬНО в каждой POST форме #}
    <input type="text" name="username">
    <button type="submit">Submit</button>
</form>

В AJAX запросах

// Получить CSRF token из 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;
}

const csrftoken = getCookie('csrftoken');

// Fetch API
fetch('/api/endpoint/', {
    method: 'POST',
    headers: {
        'X-CSRFToken': csrftoken,
        'Content-Type': 'application/json',
    },
    body: JSON.stringify(data)
})

// Или установить глобально для всех запросов
// (если используете axios или другую библиотеку)
axios.defaults.headers.common['X-CSRFToken'] = csrftoken;

Когда можно отключить CSRF

from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator

# ❌ ОПАСНО! Используйте только для:
# 1. Публичных API без авторизации
# 2. Webhook endpoints (с другой аутентификацией)

@csrf_exempt
def webhook_receiver(request):
    # ОБЯЗАТЕЛЬНО проверить подпись webhook!
    signature = request.headers.get('X-Hub-Signature')
    if not verify_signature(request.body, signature):
        return HttpResponseForbidden()
    # ...

# Для class-based views
@method_decorator(csrf_exempt, name='dispatch')
class WebhookView(View):
    def post(self, request):
        # ...

Дополнительные настройки CSRF

# settings.py

# Доверенные origins для CSRF (для CORS requests)
CSRF_TRUSTED_ORIGINS = [
    'https://yourdomain.com',
    'https://*.yourdomain.com',
]

# Кастомный header name
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'

# Cookie settings
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_AGE = 31449600  # 1 год
CSRF_COOKIE_DOMAIN = '.yourdomain.com'  # Для поддоменов
CSRF_COOKIE_PATH = '/'
CSRF_COOKIE_SECURE = True  # HTTPS only
CSRF_COOKIE_HTTPONLY = True  # Защита от XSS
CSRF_COOKIE_SAMESITE = 'Lax'  # Strict или Lax

Результат: Защита от подделки запросов со сторонних сайтов.


5. SQL Injection — почему ORM не панацея

Безопасно: ORM

# ✅ БЕЗОПАСНО - параметризованные запросы
username = request.GET.get('username')
users = User.objects.filter(username=username)

# ✅ БЕЗОПАСНО - Q objects
from django.db.models import Q
User.objects.filter(
    Q(username=username) | Q(email=email)
)

# ✅ БЕЗОПАСНО - даже со строками
User.objects.filter(username__contains=user_input)

SQL под капотом:

SELECT * FROM auth_user
WHERE username = %s  -- Параметр экранирован!

Опасно: Raw SQL

# ❌ ОПАСНО! SQL Injection
username = request.GET.get('username')
User.objects.raw(
    f"SELECT * FROM auth_user WHERE username = '{username}'"
)

# Атака: ?username=' OR '1'='1
# Результат: SELECT * FROM auth_user WHERE username = '' OR '1'='1'
# → Вернёт ВСЕХ пользователей!

Правильное использование Raw SQL

# ✅ БЕЗОПАСНО - параметризованный запрос
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 AND active = %s",
        [username, True]
    )
    results = cursor.fetchall()

Опасность в extra() и raw()

# ❌ ОПАСНО!
User.objects.extra(
    where=[f"username = '{user_input}'"]
)

# ✅ БЕЗОПАСНО
User.objects.extra(
    where=["username = %s"],
    params=[user_input]
)

NoSQL Injection (MongoDB, etc.)

# Даже с NoSQL ORM нужна осторожность
# ❌ ОПАСНО (если используете djongo или mongoengine)
filter_query = json.loads(request.body)  # Пользовательский ввод!
Document.objects.filter(**filter_query)  # Может содержать $where, $ne, etc.

# ✅ БЕЗОПАСНО - валидация структуры
allowed_fields = ['username', 'email', 'age']
filter_query = {
    k: v for k, v in user_input.items()
    if k in allowed_fields
}

Результат: БД защищена от injection атак.


6. XSS (Cross-Site Scripting)

Типы XSS

ТипОписаниеПример
ReflectedВредоносный код в URL/параметрах?q=<script>steal()</script>
StoredКод сохранён в БДКомментарий с <script>
DOM-basedJS манипуляция DOMinnerHTML = user_input

Django автоматическая защита

{# ✅ БЕЗОПАСНО - автоэкранирование #}
<p>Hello, {{ user.username }}!</p>

{# Если username = "<script>alert('XSS')</script>" #}
{# Результат: <p>Hello, &lt;script&gt;alert('XSS')&lt;/script&gt;!</p> #}

Когда отключается экранирование

{# ❌ ОПАСНО! |safe отключает экранирование #}
{{ user_comment|safe }}

{# ❌ ОПАСНО! #}
{% autoescape off %}
    {{ user_input }}
{% endautoescape %}

{# ✅ Используйте safe только для доверенного контента #}
{# Например, markdown после санитизации: #}
{{ content|markdown|safe }}

Санитизация HTML

# Установка
pip install bleach

# Использование
import bleach

ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'a']
ALLOWED_ATTRS = {'a': ['href', 'title']}

def sanitize_html(dirty_html):
    return bleach.clean(
        dirty_html,
        tags=ALLOWED_TAGS,
        attributes=ALLOWED_ATTRS,
        strip=True  # Удалить запрещённые теги
    )

# В view
clean_comment = sanitize_html(request.POST.get('comment'))
comment.content = clean_comment
comment.save()

JSON и JavaScript context

{# ❌ ОПАСНО! #}
<script>
    var userData = {{ user_data|safe }};  // XSS если в user_data есть </script>
</script>

{# ✅ БЕЗОПАСНО - используйте json_script #}
{{ user_data|json_script:"user-data" }}
<script>
    const userData = JSON.parse(
        document.getElementById('user-data').textContent
    );
</script>

Content Security Policy (CSP)

# pip install django-csp

# settings.py
MIDDLEWARE = [
    # ...
    'csp.middleware.CSPMiddleware',
]

# CSP политика
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = (
    "'self'",
    'https://cdn.jsdelivr.net',  # Trusted CDN
)
CSP_STYLE_SRC = (
    "'self'",
    "'unsafe-inline'",  # Только если необходимо
)
CSP_IMG_SRC = ("'self'", 'data:', 'https:')
CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com')
CSP_CONNECT_SRC = ("'self'", 'https://api.yourdomain.com')

# Reporting
CSP_REPORT_URI = '/csp-report/'
CSP_REPORT_ONLY = False  # True для тестирования

# views.py - CSP report endpoint
@csrf_exempt
def csp_report(request):
    if request.method == 'POST':
        logger.warning('CSP Violation: %s', request.body)
    return HttpResponse()

Результат: XSS атаки блокируются на всех уровнях.


7. Аутентификация и Session Security

Password Hashing

# settings.py
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',  # Самый стойкий
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',  # По умолчанию
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]

# Для Argon2
# pip install django[argon2]

Password Validation

# settings.py
AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
            'min_length': 12,  # Минимум 12 символов
        }
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
    # Кастомный валидатор
    {
        'NAME': 'myapp.validators.PasswordComplexityValidator',
    },
]

# myapp/validators.py
import re
from django.core.exceptions import ValidationError

class PasswordComplexityValidator:
    def validate(self, password, user=None):
        if not re.search(r'[A-Z]', password):
            raise ValidationError("Password must contain uppercase letter")
        if not re.search(r'[a-z]', password):
            raise ValidationError("Password must contain lowercase letter")
        if not re.search(r'[0-9]', password):
            raise ValidationError("Password must contain digit")
        if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            raise ValidationError("Password must contain special character")

    def get_help_text(self):
        return "Password must contain uppercase, lowercase, digit, and special character"

Brute-Force Protection

# pip install django-axes

# settings.py
INSTALLED_APPS = [
    # ...
    'axes',
]

MIDDLEWARE = [
    # AxesMiddleware должен быть ПОСЛЕ AuthenticationMiddleware
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'axes.middleware.AxesMiddleware',
]

AUTHENTICATION_BACKENDS = [
    'axes.backends.AxesStandaloneBackend',  # AxesBackend первым!
    'django.contrib.auth.backends.ModelBackend',
]

# Настройки Axes
AXES_FAILURE_LIMIT = 5  # Блокировка после 5 неудачных попыток
AXES_COOLOFF_TIME = 1  # Час блокировки
AXES_LOCKOUT_MESSAGE = 'Too many failed attempts. Try again later.'
AXES_RESET_ON_SUCCESS = True  # Сброс счётчика после успешного входа
AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP = True  # По user + IP

# Whitelist IP
AXES_IP_WHITELIST = ['127.0.0.1']
AXES_NEVER_LOCKOUT_WHITELIST = True

# Blacklist
AXES_IP_BLACKLIST = []
AXES_ONLY_USER_FAILURES = False  # False = по IP тоже блокирует

Two-Factor Authentication (2FA)

# pip install django-otp qrcode

# settings.py
INSTALLED_APPS = [
    # ...
    'django_otp',
    'django_otp.plugins.otp_totp',
]

MIDDLEWARE = [
    # ...
    'django_otp.middleware.OTPMiddleware',
]

# views.py
from django_otp.decorators import otp_required

@otp_required
def sensitive_view(request):
    # Требует 2FA
    pass

# Для админки
from django_otp.admin import OTPAdminSite
admin.site.__class__ = OTPAdminSite

Session Security

# settings.py

# Session настройки
SESSION_COOKIE_AGE = 1209600  # 2 недели
SESSION_SAVE_EVERY_REQUEST = False  # True = обновлять при каждом запросе
SESSION_EXPIRE_AT_BROWSER_CLOSE = False  # True = сессия только на время браузера

# Security
SESSION_COOKIE_HTTPONLY = True  # Защита от XSS
SESSION_COOKIE_SECURE = True  # HTTPS only
SESSION_COOKIE_SAMESITE = 'Lax'  # Strict, Lax, или None

# Session backend (Redis для production)
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'

# Или полностью в Redis
# pip install django-redis
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'

Результат: Многоуровневая защита учётных записей.


8. File Upload Security

Опасности загрузки файлов

  1. Remote Code Execution: Загрузка .php, .py, .jsp на сервер
  2. Path Traversal: ../../etc/passwd в имени файла
  3. XSS: Загрузка HTML/SVG с JavaScript
  4. DoS: Огромные файлы
  5. Malware: Вирусы в файлах

Валидация типов файлов

# models.py
from django.core.exceptions import ValidationError
import os
import magic  # python-magic

ALLOWED_EXTENSIONS = ['.pdf', '.jpg', '.jpeg', '.png', '.doc', '.docx']
ALLOWED_MIMETYPES = [
    'application/pdf',
    'image/jpeg',
    'image/png',
    'application/msword',
]

def validate_file_extension(value):
    ext = os.path.splitext(value.name)[1].lower()
    if ext not in ALLOWED_EXTENSIONS:
        raise ValidationError(
            f'Unsupported file extension. Allowed: {", ".join(ALLOWED_EXTENSIONS)}'
        )

def validate_file_mimetype(value):
    # ВНИМАНИЕ: Не доверяйте только расширению!
    # Проверяйте реальный MIME type
    file_mime = magic.from_buffer(value.read(1024), mime=True)
    value.seek(0)  # Вернуть указатель в начало

    if file_mime not in ALLOWED_MIMETYPES:
        raise ValidationError(f'Invalid file type: {file_mime}')

def validate_file_size(value):
    filesize = value.size
    if filesize > 5 * 1024 * 1024:  # 5MB
        raise ValidationError('Max file size is 5MB')

class Document(models.Model):
    upload = models.FileField(
        upload_to='documents/%Y/%m/%d/',
        validators=[
            validate_file_extension,
            validate_file_mimetype,
            validate_file_size,
        ]
    )

Безопасное имя файла

# Никогда не используйте оригинальное имя напрямую!
import uuid
from django.utils.text import slugify

def secure_filename(filename):
    # Удалить путь
    filename = os.path.basename(filename)
    # Slugify имени
    name, ext = os.path.splitext(filename)
    name = slugify(name)
    # Добавить UUID для уникальности
    return f"{name}-{uuid.uuid4().hex[:8]}{ext}"

# В модели
def upload_path(instance, filename):
    return f'uploads/{instance.user.id}/{secure_filename(filename)}'

class Document(models.Model):
    file = models.FileField(upload_to=upload_path)

Ограничения на уровне Django

# settings.py

# Максимальный размер загружаемых данных
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880  # 5MB в памяти
FILE_UPLOAD_MAX_MEMORY_SIZE = 5242880  # 5MB в памяти

# Временные файлы
FILE_UPLOAD_TEMP_DIR = '/tmp/django-uploads'
FILE_UPLOAD_PERMISSIONS = 0o644

# Хранение файлов вне MEDIA_ROOT для исполняемых
PRIVATE_MEDIA_ROOT = '/var/app/private-media/'

Сканирование на вирусы

# pip install pyclamd

import pyclamd

def scan_uploaded_file(file):
    cd = pyclamd.ClamdUnixSocket()

    # Проверить что ClamAV доступен
    if not cd.ping():
        raise Exception('ClamAV is not running')

    # Сканировать файл
    scan_result = cd.scan_stream(file.read())
    file.seek(0)

    if scan_result:
        raise ValidationError('File contains malware!')

# В view или form
def clean_file(self):
    file = self.cleaned_data.get('file')
    scan_uploaded_file(file)
    return file

CDN и изоляция

# settings.py - Хранить загрузки отдельно
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'

# Отдельный bucket для пользовательских загрузок
USER_UPLOADS_BUCKET = 'user-uploads-isolated'

# Content-Disposition для скачивания вместо отображения
AWS_S3_OBJECT_PARAMETERS = {
    'ContentDisposition': 'attachment',  # Принудительное скачивание
}

Результат: Загрузки файлов не создают уязвимостей.


9. Security Headers

# settings.py

# X-Frame-Options (защита от clickjacking)
X_FRAME_OPTIONS = 'DENY'  # Или 'SAMEORIGIN'

# X-Content-Type-Options
SECURE_CONTENT_TYPE_NOSNIFF = True

# X-XSS-Protection (legacy, но всё ещё полезен)
SECURE_BROWSER_XSS_FILTER = True

# Referrer-Policy
SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
# Опции: no-referrer, origin, strict-origin, etc.

# Permissions-Policy (ранее Feature-Policy)
PERMISSIONS_POLICY = {
    "geolocation": [],  # Отключить для всех
    "camera": [],
    "microphone": [],
    "payment": ["self"],  # Только для своего origin
}

Кастомный middleware для headers

# middleware.py
class SecurityHeadersMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)

        # Permissions-Policy
        response['Permissions-Policy'] = 'geolocation=(), camera=(), microphone=()'

        # Expect-CT (Certificate Transparency)
        response['Expect-CT'] = 'max-age=86400, enforce'

        # NEL (Network Error Logging)
        response['NEL'] = '{"report_to":"default","max_age":31536000}'

        return response

# settings.py
MIDDLEWARE = [
    'myapp.middleware.SecurityHeadersMiddleware',
    # ...
]

Результат: Браузеры применяют дополнительные защитные механизмы.


10. Dependencies Security

Регулярная проверка уязвимостей

# pip-audit (официальный инструмент от PyPA)
pip install pip-audit
pip-audit

# Safety
pip install safety
safety check

# С GitHub Advisory Database
safety check --json

# Snyk (более продвинутый)
npm install -g snyk
snyk test --file=requirements.txt

Автоматизация в CI/CD

# .github/workflows/security.yml
name: Security Check

on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install pip-audit safety

      - name: Run pip-audit
        run: pip-audit --requirement requirements.txt

      - name: Run safety check
        run: safety check --file requirements.txt --json

      - name: Run bandit (code security)
        run: |
          pip install bandit
          bandit -r . -f json -o bandit-report.json

Dependabot configuration

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "security-team"
    labels:
      - "security"
      - "dependencies"

Результат: Уязвимости в зависимостях детектируются автоматически.


Финальный Production Checklist

Критически важно (нельзя игнорировать)

  • ✅ DEBUG = False
  • ✅ SECRET_KEY из environment variables
  • ✅ ALLOWED_HOSTS настроен корректно
  • ✅ HTTPS настроен с валидным сертификатом
  • ✅ SESSION_COOKIE_SECURE = True
  • ✅ CSRF_COOKIE_SECURE = True
  • ✅ SECURE_SSL_REDIRECT = True
  • ✅ python manage.py check --deploy без ошибок
  • ✅ Все credentials в .env (не в коде!)
  • ✅ Database user с минимальными привилегиями

Обязательно (сильно рекомендуется)

  • ✅ HSTS настроен (SECURE_HSTS_SECONDS)
  • ✅ CSP (Content Security Policy) активен
  • ✅ File upload validation
  • ✅ Rate limiting (django-axes или django-ratelimit)
  • ✅ Logging настроен корректно
  • ✅ Error monitoring (Sentry)
  • ✅ Regular backups (автоматические)
  • ✅ pip-audit в CI/CD pipeline

Настоятельно рекомендуется

  • ✅ WAF (Web Application Firewall) - CloudFlare, AWS WAF
  • ✅ 2FA для администраторов
  • ✅ Penetration testing (ежегодно)
  • ✅ Security code review process
  • ✅ Secrets scanning в Git (git-secrets, truffleHog)
  • ✅ GDPR/CCPA compliance (если applicable)
  • ✅ Security.txt (/.well-known/security.txt)
  • ✅ Bug bounty program (для крупных проектов)

Автоматизация проверок

Django check --deploy

# Запустить все security проверки
python manage.py check --deploy

# Пример вывода предупреждений:
# WARNINGS:
# ?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting.
# ?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True.
# ?: (security.W012) SESSION_COOKIE_SECURE is not set to True.

Pre-commit hooks

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pycqa/bandit
    rev: '1.7.5'
    hooks:
      - id: bandit
        args: ['-r', 'myapp/', '-f', 'screen']

  - repo: https://github.com/PyCQA/flake8
    rev: '6.1.0'
    hooks:
      - id: flake8
        additional_dependencies: [flake8-bugbear, flake8-security]

# Установка
pip install pre-commit
pre-commit install

Инструменты и ресурсы

Обязательные инструменты

  • django-axes — Brute-force protection
  • django-csp — Content Security Policy
  • pip-audit — Vulnerability scanning
  • bandit — Code security linter
  • safety — Dependency checker

Сканеры и тестирование

  • OWASP ZAP — Automated security testing
  • Burp Suite — Professional security testing
  • Nikto — Web server scanner
  • SQLMap — SQL injection testing

Мониторинг

  • Sentry — Error & security monitoring
  • DataDog — APM & security
  • Wazuh — Security monitoring & threat detection

Образовательные ресурсы

  • OWASP Top 10
  • Django Security Releases
  • PortSwigger Web Security Academy
  • Mozilla Web Security

Compliance и Regulations

GDPR (EU)

# Право на забвение (Right to be forgotten)
class User(AbstractUser):
    def anonymize(self):
        """GDPR compliant user anonymization"""
        self.email = f"deleted_{self.id}@example.com"
        self.first_name = "Deleted"
        self.last_name = "User"
        self.is_active = False
        self.save()

        # Удалить или анонимизировать связанные данные
        self.profile.delete()
        self.comments.all().update(content="[deleted]")

# Data export
def export_user_data(user):
    """GDPR compliant data export"""
    return {
        'profile': UserSerializer(user).data,
        'comments': CommentSerializer(user.comments.all(), many=True).data,
        'orders': OrderSerializer(user.orders.all(), many=True).data,
    }

PCI DSS (Payment Card Industry)

# НИКОГДА не храните:
# - Полный номер карты (только last 4 digits)
# - CVV/CVC
# - PIN

class Payment(models.Model):
    # ✅ Только последние 4 цифры
    card_last4 = models.CharField(max_length=4)

    # ✅ Токен от payment gateway (Stripe, etc.)
    payment_token = models.CharField(max_length=255)

    # ❌ НИКОГДА так не делайте!
    # card_number = models.CharField(max_length=16)  # ЗАПРЕЩЕНО!
    # cvv = models.CharField(max_length=3)  # ЗАПРЕЩЕНО!

Заключение

Безопасность Django-приложения — это не чек-лист, который можно пройти один раз. Это непрерывный процесс:

  1. Проектирование: Security by design
  2. Разработка: Secure coding practices
  3. Тестирование: Automated security tests
  4. Деплой: Hardened configuration
  5. Мониторинг: Real-time threat detection
  6. Обновление: Regular patches and updates

Ключевые принципы:

  • Defense in Depth: Многоуровневая защита
  • Least Privilege: Минимальные права доступа
  • Fail Securely: Безопасный режим при ошибках
  • Don't Trust Input: Валидация всех данных
  • Keep it Simple: Простота = безопасность

Помните:

"Безопасность — это цепь. Она настолько сильна, насколько сильно её самое слабое звено."

Начните с python manage.py check --deploy и исправьте все предупреждения. Остальное приложится!

Нужна помощь с Django?

Берём проекты на поддержку с чётким SLA. Стабилизируем за 2 недели, даём план развития на 90 дней. 15+ лет опыта с Django.

Обсудить проектНаши услуги
Предыдущая статья
Производительность

10 способов ускорить Django в 2-3 раза

Следующая статья
DevOps

Деплой Django: от gunicorn до Kubernetes

•