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":"Безопасность"}
Безопасность Django-приложения — это многоуровневая защита. Ключевые моменты:
Результат: Защищенное приложение, готовое к audit и compliance проверкам.
Django имеет отличную репутацию в области безопасности благодаря встроенной защите от большинства OWASP Top 10 уязвимостей. Однако, неправильная конфигурация может свести все это на нет.
Статистика:
95% успешных атак на Django-приложения происходят из-за misconfiguration, а не из-за уязвимостей в самом фреймворке. — OWASP Report 2023
В этой статье — полный security checklist с примерами реальных атак и методами защиты.
| 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 |
SECRET_KEY используется для:
signing module)Если SECRET_KEY утекёт → атакующий может:
# ❌ ОПАСНО! Никогда так не делайте
SECRET_KEY = 'django-insecure-hardcoded-key-123456'
# ❌ ОПАСНО! В коде
SECRET_KEY = '7d@kw!_x9%m&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))'
Если ключ скомпрометирован:
# settings.py
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
# Старый ключ для валидации существующих токенов
SECRET_KEY_FALLBACKS = [
os.environ.get('DJANGO_SECRET_KEY_OLD'),
]
# После ротации все сессии будут инвалидированы
# Пользователи должны войти заново
⚠️ Важно: После смены SECRET_KEY:
Результат: Основа для всех cryptographic операций защищена.
При DEBUG=True Django показывает:
Детальные tracebacks с:
Настройки проекта (settings.py)
SQL-запросы с данными
Стек вызовов с логикой приложения
# 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
# 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)
Результат: Атакующий не получает информацию о структуре приложения.
Без 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'
Что это: Браузеры предзагружают список сайтов, которые ВСЕГДА должны открываться через HTTPS.
Как добавить сайт:
SECURE_HSTS_PRELOAD = True⚠️ Важно: Удаление из preload list может занять месяцы!
# Установка 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/
Результат: Все данные передаются в зашифрованном виде.
Сценарий:
bank.comevil.comevil.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>
# 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>
// Получить 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;
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):
# ...
# 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
Результат: Защита от подделки запросов со сторонних сайтов.
# ✅ БЕЗОПАСНО - параметризованные запросы
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 -- Параметр экранирован!
# ❌ ОПАСНО! 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'
# → Вернёт ВСЕХ пользователей!
# ✅ БЕЗОПАСНО - параметризованный запрос
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()
# ❌ ОПАСНО!
User.objects.extra(
where=[f"username = '{user_input}'"]
)
# ✅ БЕЗОПАСНО
User.objects.extra(
where=["username = %s"],
params=[user_input]
)
# Даже с 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 атак.
| Тип | Описание | Пример |
|---|---|---|
| Reflected | Вредоносный код в URL/параметрах | ?q=<script>steal()</script> |
| Stored | Код сохранён в БД | Комментарий с <script> |
| DOM-based | JS манипуляция DOM | innerHTML = user_input |
{# ✅ БЕЗОПАСНО - автоэкранирование #}
<p>Hello, {{ user.username }}!</p>
{# Если username = "<script>alert('XSS')</script>" #}
{# Результат: <p>Hello, <script>alert('XSS')</script>!</p> #}
{# ❌ ОПАСНО! |safe отключает экранирование #}
{{ user_comment|safe }}
{# ❌ ОПАСНО! #}
{% autoescape off %}
{{ user_input }}
{% endautoescape %}
{# ✅ Используйте safe только для доверенного контента #}
{# Например, markdown после санитизации: #}
{{ content|markdown|safe }}
# Установка
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()
{# ❌ ОПАСНО! #}
<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>
# 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 атаки блокируются на всех уровнях.
# 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]
# 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"
# 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 тоже блокирует
# 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
# 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'
Результат: Многоуровневая защита учётных записей.
.php, .py, .jsp на сервер../../etc/passwd в имени файла# 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)
# 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
# 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', # Принудительное скачивание
}
Результат: Загрузки файлов не создают уязвимостей.
# 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.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',
# ...
]
Результат: Браузеры применяют дополнительные защитные механизмы.
# 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
# .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
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "security-team"
labels:
- "security"
- "dependencies"
Результат: Уязвимости в зависимостях детектируются автоматически.
DEBUG = FalseSECRET_KEY из environment variablesALLOWED_HOSTS настроен корректноSESSION_COOKIE_SECURE = TrueCSRF_COOKIE_SECURE = TrueSECURE_SSL_REDIRECT = Truepython manage.py check --deploy без ошибок.env (не в коде!)SECURE_HSTS_SECONDS)/.well-known/security.txt)# Запустить все 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-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
# Право на забвение (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,
}
# НИКОГДА не храните:
# - Полный номер карты (только 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-приложения — это не чек-лист, который можно пройти один раз. Это непрерывный процесс:
Ключевые принципы:
Помните:
"Безопасность — это цепь. Она настолько сильна, насколько сильно её самое слабое звено."
Начните с python manage.py check --deploy и исправьте все предупреждения. Остальное приложится!
Берём проекты на поддержку с чётким SLA. Стабилизируем за 2 недели, даём план развития на 90 дней. 15+ лет опыта с Django.