# Django (Python) Web Security Spec (Django 6.0.x, Python 3.x) This document is designed as a **security spec** that supports: 1. **Secure-by-default code generation** for new Django code. 2. **Security review / vulnerability hunting** in existing Django code (passive “notice issues while working” and active “scan the repo and report findings”). It is intentionally written as a set of **normative requirements** (“MUST/SHOULD/MAY”) plus **audit rules** (what bad patterns look like, how to detect them, and how to fix/mitigate them). --- ## 0) Safety, boundaries, and anti-abuse constraints (MUST FOLLOW) * MUST NOT request, output, log, or commit secrets (API keys, passwords, private keys, session cookies, `SECRET_KEY`, `SECRET_KEY_FALLBACKS`, database passwords). * MUST NOT “fix” security by disabling protections (e.g., removing `CsrfViewMiddleware`, sprinkling `@csrf_exempt`, loosening `ALLOWED_HOSTS` to `['*']`, disabling `SecurityMiddleware`, disabling template auto-escaping, disabling permission checks). * MUST provide **evidence-based findings** during audits: cite file paths, code snippets, and concrete configuration values that justify the claim. * MUST treat uncertainty honestly: if a protection might exist in infrastructure (reverse proxy, WAF, CDN, ingress controller), report it as “not visible in app code; verify at runtime / edge config”. * MUST keep fixes compatible with Django’s intended security model: prefer Django’s built-ins (middleware, auth, forms, ORM) over custom security logic whenever possible. Django’s deployment checklist and system checks are part of the intended model. ([Django Project][1]) --- ## 1) Operating modes ### 1.1 Generation mode (default) When asked to write new Django code or modify existing code: * MUST follow every **MUST** requirement in this spec. * SHOULD follow every **SHOULD** requirement unless the user explicitly says otherwise. * MUST prefer safe-by-default Django APIs and proven libraries over custom security code. * MUST avoid introducing new risky sinks (dynamic template rendering from untrusted strings, unsafe redirects, unsafe file serving, shell execution, raw SQL string formatting, SSRF-capable URL fetchers from untrusted input). ### 1.2 Passive review mode (always on while editing) While working anywhere in a Django repo (even if the user did not ask for a security scan): * MUST “notice” violations of this spec in touched/nearby code. * SHOULD mention issues as they come up, with a brief explanation + safe fix. ### 1.3 Active audit mode (explicit scan request) When the user asks to “scan”, “audit”, or “hunt for vulns”: * MUST systematically search the codebase for violations of this spec. * MUST output findings in a structured format (see §2.3). Recommended audit order: 1. Deployment entrypoints (ASGI/WSGI), Dockerfiles, Procfiles, systemd units, platform manifests. 2. `settings.py` and environment-specific settings modules. 3. Middleware ordering and enabled protections. 4. Authn/authz (login, session management, permissions, admin). 5. CSRF protections and state-changing endpoints. 6. Templates and XSS. 7. File handling (uploads/downloads/static/media) and path traversal. 8. Injection classes (SQL, command execution, unsafe deserialization). 9. Outbound requests (SSRF). 10. Redirect handling (open redirects) + CORS + security headers (CSP, HSTS, etc.). 11. Dependency/pinning and patch posture. --- ## 2) Definitions and review guidance ### 2.1 Untrusted input (treat as attacker-controlled unless proven otherwise) Examples include: * `request.GET`, `request.POST`, `request.FILES` * `request.body`, JSON bodies (e.g., `json.loads(request.body)`), DRF `request.data` * URL path parameters (e.g., ``, ``) * `request.headers` / `request.META` (including `HTTP_HOST`, `HTTP_ORIGIN`, `HTTP_REFERER`, `HTTP_X_FORWARDED_*`) * `request.COOKIES` * Any data from external systems (webhooks, third-party APIs, message queues) * Any persisted content that originated from users (DB rows, cached content, file uploads) Django explicitly emphasizes “never trust user-controlled data” and recommends using forms/validation. ([Django Project][2]) ### 2.2 State-changing request A request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook send), or initiate privileged actions. ### 2.3 Required audit finding format For each issue found, output: * Rule ID: * Severity: Critical / High / Medium / Low * Location: file path + function/class/view name + line(s) * Evidence: the exact code/config snippet * Impact: what could go wrong, who can exploit it * Fix: safe change (prefer minimal diff) * Mitigation: defense-in-depth if immediate fix is hard * False positive notes: what to verify if uncertain --- ## 3) Secure baseline: minimum production configuration (MUST in production) This is the smallest “production baseline” that prevents common Django misconfigurations. Django provides a “Deployment checklist” and recommends running `manage.py check --deploy` against production settings. ([Django Project][1]) ### 3.1 Settings management pattern (SHOULD) * SHOULD use environment-based configuration (or a secret manager) so production settings are not hard-coded. * MUST treat sensitive settings as confidential (e.g., `SECRET_KEY`, DB passwords) and keep them out of source control. Django’s checklist explicitly recommends loading `SECRET_KEY` from env or a file rather than hardcoding. ([Django Project][1]) * SHOULD separate dev vs prod settings modules, with safe defaults for production (fail closed if critical settings are missing). ([Django Project][1]) ### 3.2 Minimum baseline targets (production) * MUST NOT use `manage.py runserver` as the production entrypoint; use a production-ready WSGI or ASGI server. ([Django Project][1]) * MUST set `DEBUG = False` in production. ([Django Project][1]) * MUST set a strong, secret `SECRET_KEY` and keep it secret; MAY use `SECRET_KEY_FALLBACKS` for safe rotation. ([Django Project][1]) * MUST set `ALLOWED_HOSTS` to expected hosts (no wildcard unless you do your own host validation). ([Django Project][1]) * MUST enforce HTTPS for authenticated areas (ideally site-wide for any login-capable app) and set `CSRF_COOKIE_SECURE=True` and `SESSION_COOKIE_SECURE=True` when HTTPS is used. ([Django Project][1]) * SHOULD enable key `SecurityMiddleware` headers/settings: HSTS, Referrer-Policy, COOP, nosniff, SSL redirect (with correct proxy configuration). ([Django Project][3]) * MUST treat user uploads as untrusted; ensure your web server never interprets them as executable content; keep `MEDIA_ROOT` separate from `STATIC_ROOT`. ([Django Project][1]) --- ## 4) Rules (generation + audit) Each rule contains: required practice, insecure patterns, detection hints, and remediation. ### DJANGO-DEPLOY-001: Do not use Django’s development server in production Severity: High (if production) Required: * MUST NOT deploy `manage.py runserver` as the production server. * MUST run behind a production-grade WSGI or ASGI server. ([Django Project][1]) Insecure patterns: * Production docs/scripts using `python manage.py runserver 0.0.0.0:8000`. * Docker `CMD`/entrypoint uses `runserver`. * Kubernetes/Procfile/systemd units invoking `runserver`. Detection hints: * Search for `manage.py runserver`, `runserver 0.0.0.0`, `--insecure`. * Check Docker `CMD/ENTRYPOINT`, Procfile, systemd unit files, Helm charts. Fix: * Use a production server (WSGI/ASGI) as recommended in Django’s deployment checklist. ([Django Project][1]) Note: * `runserver` is fine for local development. Only flag if it’s used as the production entrypoint. --- ### DJANGO-DEPLOY-002: `DEBUG` MUST be disabled in production Severity: High Required: * MUST set `DEBUG = False` in production. * MUST treat any mechanism that exposes debug pages/tracebacks to untrusted users as a critical information disclosure risk. Django’s checklist explicitly warns `DEBUG=True` leaks source excerpts, local variables, settings, and more. ([Django Project][1]) Insecure patterns: * `DEBUG = True` in production settings. * Environment defaults to `DEBUG=True` unless explicitly overridden. Detection hints: * Search `DEBUG = True`, `DEBUG=os.environ.get(..., True)`, `DJANGO_DEBUG`, `.env` files. * Look for “production” settings modules that import from dev defaults. Fix: * Set `DEBUG=False` in prod settings; use explicit environment config. * Ensure error reporting is via safe logging/monitoring, not debug pages. ([Django Project][1]) --- ### DJANGO-CONFIG-001: `SECRET_KEY` must be strong, secret, and rotated safely Severity: High (Critical if missing in production with signing/sessions) Required: * MUST set a large random `SECRET_KEY` in production and keep it secret. ([Django Project][1]) * MUST NOT commit it to source control or print/log it. ([Django Project][1]) * SHOULD load it from env or a file/secret store (not hard-coded). ([Django Project][1]) * MAY rotate keys using `SECRET_KEY_FALLBACKS` to avoid instantly invalidating all signed data; MUST remove old keys from fallbacks in a timely manner. ([Django Project][1]) Insecure patterns: * Hard-coded `SECRET_KEY = "..."` in repo for production. * `SECRET_KEY` reused across environments. * `SECRET_KEY_FALLBACKS` contains long-expired keys indefinitely. Detection hints: * Search for `SECRET_KEY =`, `SECRET_KEY_FALLBACKS`, `.env` committed files, `print(settings.SECRET_KEY)`. Fix: * Load from secret manager / environment variable. * If rotating: * Set new `SECRET_KEY` * Keep old key(s) temporarily in `SECRET_KEY_FALLBACKS` * Remove old key(s) after the rotation window. ([Django Project][1]) --- ### DJANGO-HOST-001: Host header must be validated (`ALLOWED_HOSTS` must be strict) Severity: Medium Required: * MUST set `ALLOWED_HOSTS` in production to your expected domains/hosts. ([Django Project][1]) * MUST NOT set `ALLOWED_HOSTS = ['*']` in production unless you also implement your own robust `Host` validation (Django warns that wildcards require your own validation to avoid CSRF-class attacks). ([Django Project][1]) * SHOULD configure the fronting web server to reject unknown hosts early (defense-in-depth). ([Django Project][1]) Insecure patterns: * `ALLOWED_HOSTS = ['*']` (or env expands to `*`) in production. * `ALLOWED_HOSTS = []` with `DEBUG=False` (site won’t run, or misconfigured deployments attempt workarounds). Detection hints: * Search `ALLOWED_HOSTS`. * Check platform environment settings that override `ALLOWED_HOSTS`. Fix: * Set `ALLOWED_HOSTS = ['example.com', 'www.example.com', ...]` for prod. * Keep dev hosts separate. Notes: * Django uses the Host header for URL construction; fake Host values can lead to CSRF, cache poisoning, and poisoned email links (Django security docs call this out). ([Django Project][2]) --- ### DJANGO-HTTPS-001: If TLS is used cookie transport must be secured Severity: High (Critical for auth-enabled apps) NOTE: Only enforce this if TLS is enabled, as it will break non-TLS applications If using TLS: * MUST set: * `CSRF_COOKIE_SECURE = True` ([Django Project][1]) * `SESSION_COOKIE_SECURE = True` ([Django Project][1]) * SHOULD consider enabling: * `SECURE_SSL_REDIRECT = True` (with correct proxy config) ([Django Project][3]) * HSTS via `SECURE_HSTS_SECONDS` (+ includeSubDomains/preload as appropriate). ([Django Project][3]) Insecure patterns: * Login pages over HTTP, or mixed HTTP/HTTPS with the same session cookie. * `CSRF_COOKIE_SECURE=False` or `SESSION_COOKIE_SECURE=False` in production HTTPS. * HSTS enabled incorrectly (can break site for the duration). Detection hints: * Inspect `settings.py` for `CSRF_COOKIE_SECURE`, `SESSION_COOKIE_SECURE`, `SECURE_SSL_REDIRECT`, `SECURE_HSTS_SECONDS`. * Inspect proxy/ingress config for HTTP->HTTPS redirect behavior. Fix: * Enable HTTPS redirect and secure cookies. * Add HSTS carefully (start with low value, validate, then increase). Django warns misconfig can break your site for the HSTS duration. ([Django Project][3]) --- ### DJANGO-PROXY-001: Reverse proxy trust must be configured correctly (`SECURE_PROXY_SSL_HEADER`) Severity: Medium (when behind a TLS proxy) Required: * If behind a reverse proxy that terminates TLS, MUST configure Django so `request.is_secure()` reflects the *external* scheme, otherwise CSRF and other logic can break. Django documents using `SECURE_PROXY_SSL_HEADER` for this. ([Django Project][3]) * MUST only set `SECURE_PROXY_SSL_HEADER` if you control the proxy (or have guarantees) and it strips inbound spoofed headers. Django explicitly warns misconfig can compromise security and lists required conditions. ([Django Project][3]) Insecure patterns: * `SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")` in an environment where the proxy does not strip user-supplied `X-Forwarded-Proto`. * Infinite redirect loops after setting `SECURE_SSL_REDIRECT=True` (often indicates proxy HTTPS detection is wrong). ([Django Project][3]) Detection hints: * Search `SECURE_PROXY_SSL_HEADER`, `SECURE_SSL_REDIRECT`. * Inspect ingress/proxy behavior for stripping forwarded headers. Fix: * Set `SECURE_PROXY_SSL_HEADER` only if the proxy strips and sets the header correctly (per Django’s documented prerequisites). ([Django Project][3]) --- ### DJANGO-SESS-001: Session cookies must use secure attributes in production Severity: Medium (Only if TLS enabled) Required (production, HTTPS): * MUST set `SESSION_COOKIE_SECURE=True` (only transmit over HTTPS). ([Django Project][3]) * MUST keep `SESSION_COOKIE_HTTPONLY=True` (Django default is `True`). ([Django Project][3]) * SHOULD keep `SESSION_COOKIE_SAMESITE='Lax'` (Django default is `Lax`) unless a justified cross-site flow requires `None`. ([Django Project][3]) * SHOULD avoid setting `SESSION_COOKIE_DOMAIN` unless you truly need cross-subdomain cookies (subdomain-wide cookies expand attack surface). Insecure patterns: * `SESSION_COOKIE_SECURE=False` in production HTTPS. IMPORTANT NOTE: Only set `Secure` in production environment when TLS is configured. When running in a local dev environment over HTTP, do not set `Secure` property on cookies. You should do this conditionally based on if the app is running in production mode. You should also include a property like `SESSION_COOKIE_SECURE` which can be used to disable `Secure` cookies when testing over HTTP. * `SESSION_COOKIE_HTTPONLY=False`. * `SESSION_COOKIE_SAMESITE=None` combined with cookie-authenticated state-changing endpoints (higher CSRF risk). Detection hints: * Search for `SESSION_COOKIE_` settings, `response.set_cookie(..., httponly=..., secure=..., samesite=...)`. Fix: * Set the above explicitly in production settings. * Validate compatibility with your auth flows. ([Django Project][3]) --- ### DJANGO-SESS-002: CSRF cookie settings must be deliberate (HttpOnly has tradeoffs) Severity: Medium Required: * SHOULD set `CSRF_COOKIE_SECURE=True` when using HTTPS/TLS. ([Django Project][3]) * SHOULD keep `CSRF_COOKIE_SAMESITE='Lax'` unless you have a cross-site requirement. Django default is `Lax`. ([Django Project][3]) * MAY set `CSRF_COOKIE_HTTPONLY=True` (default is `False`) if your frontend does not need to read the CSRF cookie. If you enable it, your JS must read the CSRF token from the DOM instead (Django documents this). ([Django Project][3]) Insecure patterns: * `CSRF_COOKIE_SECURE=False` in production HTTPS/TLS. * Setting `CSRF_COOKIE_HTTPONLY=True` but still relying on “read csrftoken cookie in JS” patterns (breaks CSRF for AJAX). * `CSRF_COOKIE_SAMESITE=None` without a clear reason. Detection hints: * Search for `CSRF_COOKIE_` settings. * Search JS for `document.cookie` usage to fetch `csrftoken`. Fix: * Align cookie settings with your CSRF token acquisition method (cookie vs DOM) as Django describes. ([Django Project][4]) --- ### DJANGO-CSRF-001: Cookie-authenticated state-changing requests MUST be CSRF-protected Severity: High Required: * MUST keep `django.middleware.csrf.CsrfViewMiddleware` enabled (it is activated by default). ([Django Project][4]) * MUST include `{% csrf_token %}` in internal POST forms; MUST NOT include it in forms that POST to external URLs (Django warns this leaks the token). ([Django Project][4]) * MUST protect all state-changing endpoints (POST/PUT/PATCH/DELETE) that rely on cookies for authentication. * For AJAX/SPA calls, MUST send the CSRF token via the `X-CSRFToken` header (or configured header name) as documented. ([Django Project][4]) * MUST be very careful with `@csrf_exempt` and use it only when absolutely necessary; if used, MUST replace CSRF with an appropriate alternative control (e.g., request signing for webhooks). Django explicitly warns about `csrf_exempt`. ([Django Project][2]) Insecure patterns: * Missing `CsrfViewMiddleware` in `MIDDLEWARE`. * `@csrf_exempt` on general-purpose authenticated views. * POST/PUT/PATCH/DELETE endpoints with session auth and no CSRF tokens. * Using GET for state-changing actions (amplifies CSRF risk). Detection hints: * Inspect `settings.py` `MIDDLEWARE` for `CsrfViewMiddleware` and its order (Django notes it should come before middleware that assumes CSRF is handled). ([Django Project][4]) * Search for `csrf_exempt`, `csrf_protect`, `ensure_csrf_cookie`. * Enumerate URL patterns for non-GET methods; confirm CSRF coverage. Fix: * Re-enable `CsrfViewMiddleware`, add CSRF tokens to forms, and add AJAX header handling. * For caching decorators: if you cache a view that needs CSRF tokens, apply `@csrf_protect` as Django documents to avoid caching a response without CSRF cookie/Vary headers. ([Django Project][4]) Notes: * When deployed with HTTPS, Django’s CSRF middleware also checks the Referer header for same-origin (Django security docs mention this). ([Django Project][2]) --- ### DJANGO-XSS-001: Prevent reflected/stored XSS in templates and HTML generation Severity: High Required: * MUST rely on Django template auto-escaping (safe-by-default) for HTML templates. Django security docs highlight that Django templates escape dangerous characters but have limitations. ([Django Project][2]) * MUST NOT disable auto-escaping broadly (`{% autoescape off %}`) unless the content is trusted or safely sanitized. ([Django Project][5]) * MUST NOT mark untrusted content as safe: * Avoid `mark_safe(...)` on user data. * Avoid `|safe` on user-controlled content. * MUST be careful about HTML context pitfalls (e.g., unquoted attributes); Django explicitly shows an example where escaping does not protect an unquoted attribute context. ([Django Project][2]) * SHOULD prefer safe HTML construction helpers (e.g., `format_html`) rather than manual concatenation that risks missing escapes. ([Django Project][6]) Insecure patterns: * `{% autoescape off %}{{ user_input }}{% endautoescape %}` * `{{ user_input|safe }}` * `mark_safe(request.GET["q"])` * Unquoted attribute injections: `