Table of Contents
- TL;DR: CSRF vs. CORS vs. CSP at a Glance
- Current State for Django Projects (Django 6.0, 5.2 LTS & 4.2)
- Why CSRF, CORS, and CSP Are Frequently Confused in Practice
- CSRF in Django: Protecting Against Forged Requests in Active Sessions
- CORS in Django: The Browser Bouncer for Cross-Origin Requests
- CSP in Django: Content Security Policy as Effective XSS Mitigation
- Architecture Guide: How CSRF, CORS, and CSP Interact in Real Systems
- Comparison Table: CSRF vs. CORS vs. CSP at a Glance
- The 5 Most Common Security Pitfalls in Django Projects
- Production-Ready Checklist: Security Settings for settings.py
- Conclusion: Defense-in-Depth for Modern Django Applications
- Frequently Asked Questions (FAQ)
Meet the Author
2026-10-14
Mastering Web Security in DjangoDjango Security: Properly Configuring CSRF, CORS, and CSP
Anyone who has encountered a CORS error in the browser knows the knee-jerk reflex: temporarily slap on Access-Control-Allow-Origin: *, refresh the tab, and move on. The problem? You rarely resolve the root issue; you merely relocate a critical security boundary. In modern Django projects, CSRF, CORS, and CSP are frequently lumped together - even though they target fundamentally different threats in the web security ecosystem. In this guide, you will learn how each mechanism works, how they interlock, and how to configure them for production in Django 6.0, modern SPAs, and REST APIs.

Prerequisites
You should have the following knowledge to use the article optimally:- Django Fundamentals (
settings.py, views, middleware architecture) - HTTP Basics (request/response headers, cookies such as
SameSite&HttpOnly, methods likePOST&OPTIONS) - Same-Origin Policy (SOP) in modern browsers
- Basics of modern web architectures (REST APIs, SPAs with Vue.js / Nuxt or React)
- Core web security concepts (OWASP Top 10, especially CSRF and XSS)
If you have any questions or if anything is unclear, feel free to use the comment section below the article.
Django ships with exceptional out-of-the-box security defaults. As an experienced Python and Django agency, our security reviews repeatedly reveal the same pattern: the real vulnerability almost never lives within the framework itself, but in its configuration. Classic missteps include prematurely applied @csrf_exempt decorators, overly permissive CORS permissions, or Content Security Policies (CSP) effectively neutralized by dozens of unsafe-inline exceptions.
Let's demystify these three pillars and cleanly separate their responsibilities.
TL;DR: CSRF vs. CORS vs. CSP at a Glance
For developers and architects in a hurry, here is the essential distinction in three sentences:
- CSRF (Cross-Site Request Forgery): Protects your application against unintended actions executed by a malicious site on behalf of an authenticated user (Write Protection).
- CORS (Cross-Origin Resource Sharing): Dictates as a browser mechanism whether client-side JavaScript from another origin is permitted to read responses from your server (Read Protection).
- CSP (Content Security Policy): Restricts as an HTTP response header which executable assets and resources (scripts, stylesheets, images, frames, connections) the browser is allowed to load and execute (Execution Protection / XSS Mitigation).
Separating these three concepts prevents dangerous misconfigurations, cuts down troubleshooting time, and significantly reduces security tickets during agile sprints.
Current State for Django Projects (Django 6.0, 5.2 LTS & 4.2)
Before diving into configurations, let's review the current version milestones across the Django ecosystem:
- Django 6.0 is the modern stable release and brings a long-awaited milestone: built-in Content Security Policy (CSP) directly inside the core framework (
django.middleware.csp.ContentSecurityPolicyMiddleware). - Django 5.2 LTS is the designated Long-Term Support release with enterprise security support extending into 2028.
- Django 4.2 LTS reached its official End-of-Life in April 2026. Existing production deployments must be migrated to an active release without delay.
- No mandatory third-party package needed for CSP in Django 6.0+: While older versions rely on
django-csp, greenfield 6.0 setups should leverage the framework's native capabilities.
Many tutorials across the web still exclusively reference django-csp. While completely valid for existing 5.2 LTS codebases, new Django 6.0 architectures should prioritize the built-in middleware.
Why CSRF, CORS, and CSP Are Frequently Confused in Practice
In modern distributed web architectures, these topics rarely appear in isolation. A standard enterprise stack typically encompasses:
- A Django backend serving the ORM, business logic, and admin panel.
- A decoupled Single-Page Application (SPA) frontend (built with React or Vue.js & Nuxt) hosted on a separate subdomain or CDN origin.
- A REST API powered by Django REST Framework (DRF) with session authentication for internal staff and bearer tokens for third-party consumers.
- Third-party integrations for monitoring, analytics, and hosted fonts.
- A hardened Django admin interface that must remain strictly secured without disrupting daily editorial operations.
When multiple security boundaries intersect, everything seems connected. However, you can clarify almost any review by asking three diagnostic questions:
- Is the browser being coerced into executing a state-changing write request on behalf of an active session?
→ That is a CSRF issue. - Is client-side JavaScript on Origin A attempting to read data returned by Origin B?
→ That is a CORS issue. - Is the browser authorized to fetch, parse, or execute this particular script, inline block, or external connection?
→ That is a CSP issue.
Asking these three questions resolves more than 80% of cross-origin confusion during architectural reviews.
CSRF in Django: Protecting Against Forged Requests in Active Sessions
Cross-Site Request Forgery (CSRF) exploits the browser's implicit trust in stored session cookies.
The Attack Vector
The attack model is straightforward: A user logs into your Django web application and receives a session cookie. Without logging out, they open another tab and visit a malicious website, evil.com. That site contains a hidden form or background script targeting https://your-app.com/api/account/delete/.
Because web browsers automatically attach ambient session cookies to cross-origin requests targeting your-app.com, your server would treat the forged request as genuine and execute the account deletion if no secondary defense were in place.
How Django and CsrfViewMiddleware Defend Requests
Django includes one of the industry's most robust CSRF defenses in CsrfViewMiddleware. This middleware intercepts all non-safe HTTP methods (POST, PUT, PATCH, DELETE) and verifies that a valid, masked CSRF secret accompanies the request.
In standard template-driven views, this is handled via the form tag:
<form method="post" action="/profile/save/">
{% csrf_token %}
<button type="submit">Save Profilebutton>
form>
Django checks the token submitted in the form against the value stored in the csrftoken cookie (using a masked double-submit cookie pattern). Because the browser's Same-Origin Policy prevents evil.com from reading your site's DOM or cookies, the attacker cannot forge the token.
Origin Verification and CSRF_TRUSTED_ORIGINS Since Django 4.0
Since version 4.0, Django strictly evaluates the HTTP Origin (and Referer) header on unsafe requests. When write requests originate from a different origin, Django demands that the origin be explicitly whitelisted in CSRF_TRUSTED_ORIGINS.
Crucial Rule: Since Django 4.0, all entries must include the explicit URL scheme (
https://orhttp://):
# settings.py
CSRF_TRUSTED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
"https://*.example.com", # Wildcard subdomains supported since 4.0
]
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "Lax"
If your Django application sits behind a reverse proxy or Kubernetes ingress controller (such as NGINX or Traefik), you must configure SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https"). Otherwise, Django perceives incoming HTTPS requests as unencrypted HTTP and aborts CSRF verification.
AJAX and Fetch: Sending the X-CSRFToken Header
In decoupled SPAs or asynchronous client components within the same origin, read the CSRF token from the browser cookie and pass it through the X-CSRFToken HTTP header:
// Extract csrftoken cookie value
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");
await fetch("/api/v1/profile/", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": csrftoken,
},
body: JSON.stringify({ bio: "Full-Stack Engineer" }),
});
Django REST Framework (DRF): SessionAuth vs. TokenAuth
Django REST Framework creates frequent confusion regarding CSRF enforcement:
- When using
SessionAuthentication: Djangos CSRF checks apply fully. If you rely on session cookies for browser clients, you must supply the CSRF token on write operations. - When using
TokenAuthenticationor JWT (Bearer Tokens): If clients authenticate exclusively throughAuthorization: Bearerheaders and no ambient session cookies exist in the browser context, CSRF attacks are fundamentally impossible.
Blindly applying @csrf_exempt across API viewsets is a dangerous anti-pattern. If you are not utilizing session cookies, configure DRF's authentication_classes appropriately rather than disabling framework-level security checks.
CORS in Django: The Browser Bouncer for Cross-Origin Requests
Cross-Origin Resource Sharing (CORS) is a standardized browser mechanism that provides controlled exceptions to the Same-Origin Policy (SOP).
A frequent misconception is assuming CORS acts as a server firewall. It does not:
curlignores CORS entirely.- Server-to-server HTTP calls ignore CORS entirely.
- Native mobile apps and Python scripts ignore CORS entirely.
CORS answers one single question enforced by web browsers:
Is client-side JavaScript running on Origin A permitted to read the response payload delivered by Origin B?
For deep-dive scenarios regarding Docker and proxy setups, consult our dedicated guide on Django CORS in Production.
What Defines an Origin?
Two URLs share the same origin only if their protocol, domain, and port match identically:
http://localhost:3000(Local Frontend Dev Server)http://localhost:8000(Django API Backend)
Because the ports differ, any fetch() call between them constitutes a cross-origin request. The browser will block JavaScript from inspecting the response body unless Django returns the appropriate CORS access control headers.
Preflight Requests (OPTIONS)
For requests carrying non-simple headers or content types (such as application/json), the browser automatically sends an HTTP OPTIONS preflight request first. Only when the server validates the preflight by returning compatible Access-Control-Allow-* headers will the browser proceed with the actual GET, POST, or PUT call.
Clean Setup with django-cors-headers
In the Django ecosystem, django-cors-headers remains the industry standard:
pip install django-cors-headers
Ensure the middleware is placed at the very top of your middleware pipeline:
# settings.py
INSTALLED_APPS = [
# ...
"corsheaders",
"django.contrib.auth",
"django.contrib.contenttypes",
# ...
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # Must be placed at the top!
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
# ...
]
# Explicitly whitelist client origins:
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
"https://client.example.com",
]
# Allow credentials (cookies, auth headers)
CORS_ALLOW_CREDENTIALS = True
Why CORS_ALLOW_ALL_ORIGINS = True Is a Production Red Flag
Setting CORS_ALLOW_ALL_ORIGINS = True (Access-Control-Allow-Origin: *) exposes your endpoints to any website on the internet, allowing rogue scripts to read sensitive API data.
Furthermore, if your API utilizes credentials (CORS_ALLOW_CREDENTIALS = True), browser specifications strictly disallow the wildcard *. You must define explicit domains using CORS_ALLOWED_ORIGINS.
The Interaction: CORS_ALLOWED_ORIGINS vs. CSRF_TRUSTED_ORIGINS
When your SPA lives on https://app.example.com and communicates with your Django backend on https://api.example.com using session cookies, both settings must be configured together:
# Grants the browser permission to read API responses:
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
]
# Instructs Django to accept cross-origin POST requests in CSRF verification:
CSRF_TRUSTED_ORIGINS = [
"https://app.example.com",
]
CORS_ALLOWED_ORIGINSinstructs the browser: allow JavaScript to parse responses.CSRF_TRUSTED_ORIGINSinstructs Django: accept state-changing HTTP requests from this cross-origin sender.
CSP in Django: Content Security Policy as Effective XSS Mitigation
When mitigating Cross-Site Scripting (XSS) and code injection, Content Security Policy (CSP) represents the browser's most formidable defense.
Even with Django's automatic template escaping, user-generated rich-text, careless mark_safe() invocations, compromised npm packages, or third-party widgets can introduce vulnerabilities. CSP serves as your second line of defense (Defense-in-Depth).
The Major Update: Built-in CSP Support in Django 6.0
Django 6.0 introduces native Content Security Policy support directly within the core framework, eliminating the requirement for third-party packages in new projects.
Activate it via the new middleware:
# settings.py
from django.utils.csp import CSP
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.middleware.csp.ContentSecurityPolicyMiddleware", # Built into Django 6.0!
# ...
]
SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:", "https:"],
"font-src": [CSP.SELF, "https:"],
"connect-src": [CSP.SELF, "https://api.example.com"],
"object-src": [CSP.NONE],
"base-uri": [CSP.NONE],
"frame-ancestors": [CSP.NONE],
"form-action": [CSP.SELF],
}
This baseline immediately stops the browser from loading unauthorized external scripts, neutralizes clickjacking (frame-ancestors: 'none'), and blocks rogue form actions.
Best Practice: Report-Only Mode Before Enforcing
On mature websites, applying a strict CSP immediately can inadvertently break analytics, fonts, or tracking scripts. Always test policies using Report-Only mode first:
# settings.py
from django.utils.csp import CSP
# Emits Content-Security-Policy-Report-Only headers
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:", "https:"],
"report-uri": "/csp-violations-collector/",
}
In this mode, the browser allows all assets to execute normally while sending violation telemetry to your collector endpoint. Once all legitimate assets are identified, migrate to SECURE_CSP.
Securing Inline JavaScript with Nonces and json_script
When inline scripts fail under CSP, developers often cave in and add 'unsafe-inline'. This completely undermines XSS protection.
The robust approach is using cryptographic Nonces (numbers used once). Django generates a unique pseudorandom token per HTTP request that must match across both the CSP header and script tags:
Enable the context processor in your settings:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"OPTIONS": {
"context_processors": [
# ...
"django.template.context_processors.csp", # Exposes csp_nonce
],
},
},
]
In your templates:
{{ app_config|json_script:"app-config-data" }}
<script nonce="{{ csp_nonce }}">
const appConfig = JSON.parse(
document.getElementById("app-config-data").textContent
);
console.log("App initialized with nonce protection");
script>
Pro Tip: Always serialize variables into frontend templates using Django's built-in
json_scriptfilter rather than manual string interpolation. This completely neutralizes script escape vulnerabilities.
CSP in Django 5.2 LTS and Earlier with django-csp 4.0
For projects running on Django 5.2 LTS or Django 4.2, django-csp remains the premier tool. Note that django-csp 4.0 adopted modern dictionary configuration syntax:
# settings.py (for django-csp 4.0+)
from csp.constants import SELF, NONCE, NONE
MIDDLEWARE = [
# ...
"csp.middleware.CSPMiddleware",
]
CONTENT_SECURITY_POLICY = {
"DIRECTIVES": {
"default-src": [SELF],
"script-src": [SELF, NONCE],
"style-src": [SELF],
"object-src": [NONE],
"base-uri": [NONE],
},
}
Legacy settings such as CSP_DEFAULT_SRC = ("'self'",) are deprecated in 4.0 and should be refactored.
Architecture Guide: How CSRF, CORS, and CSP Interact in Real Systems
Reviewing common application architectures highlights when and where each security layer applies:
Scenario 1: Classic Server-Rendered Django Monolith
- CSRF: Mandatory. Every state-changing form must include
{% csrf_token %}. - CORS: Not required. Because HTML forms and views originate from the identical domain (
same-origin), CORS never triggers. - CSP: Highly recommended. Essential for hardening admin interfaces, user-generated content, and form handling against stored XSS.
Scenario 2: Decoupled SPA (React / Vue / Nuxt) with Session Cookies
- CSRF: Mandatory. The client must supply the token in the
X-CSRFTokenrequest header.CSRF_TRUSTED_ORIGINSmust include the SPA origin. - CORS: Mandatory.
CORS_ALLOWED_ORIGINSmust authorize the frontend origin, andCORS_ALLOW_CREDENTIALS = Truemust be enabled. - CSP: Mandatory. Restrictive CSP policies should be served on both the backend API domain and the SPA host.
Scenario 3: Public REST API with Token or Bearer Authentication
- CSRF: Not required. Without ambient cookie authentication, CSRF cannot occur.
- CORS: Required, if web browser applications are expected to consume the API directly via Fetch or XMLHttpRequest.
- CSP: Not applicable to pure JSON responses, but recommended on interactive API documentation routes (Swagger UI / ReDoc).
Comparison Table: CSRF vs. CORS vs. CSP at a Glance
Use this matrix during security audits and architectural design sessions:
| Criterion | CSRF (Cross-Site Request Forgery) | CORS (Cross-Origin Resource Sharing) | CSP (Content Security Policy) |
|---|---|---|---|
| Primary Objective | Prevents unauthorized actions executed under authenticated user sessions | Controls cross-domain read access to HTTP responses in client scripts | Prevents XSS, clickjacking, and malicious code injection in the browser |
| Protection Vector | Inbound (write requests) | Outbound (read responses) | Client Execution (browser runtime) |
| Enforcing Agent | Server & Middleware (validates token & origin) | Web Browser (validates Access-Control headers) | Web Browser (blocks undeclared resources) |
| Typical Attack | Phishing site triggers funds transfer or profile deletion in active session | Rogue domain silently reads confidential API data via client-side fetch | Attacker injects malicious JavaScript (session hijacking, keylogging) |
| Relevant Django Tools | CsrfViewMiddleware, CSRF_TRUSTED_ORIGINS, {% csrf_token %} | Package django-cors-headers, CORS_ALLOWED_ORIGINS | Django 6.0 SECURE_CSP or package django-csp |
| Common Misconception | "My REST API never needs CSRF protection" | "CORS protects my server from hacker attacks" | "CSP is optional since template escaping is enough" |
The 5 Most Common Security Pitfalls in Django Projects
1. Using @csrf_exempt as a Quick Fix
When hitting 403 Forbidden (CSRF verification failed), engineers often sprinkle @csrf_exempt across views. This is an invitation to exploit your users. The proper fix: verify your authentication architecture, supply the X-CSRFToken header, or configure CSRF_TRUSTED_ORIGINS accurately.
2. Leaving CORS_ALLOW_ALL_ORIGINS = True in Production
Developers frequently activate wildcard origins locally and forget to revert them before deployment. In production, only explicit origins should appear in CORS_ALLOWED_ORIGINS.
3. Enabling CSP and Promptly Giving Up with unsafe-inline
When scripts fail, teams often resort to 'unsafe-inline' and 'unsafe-eval'. Doing so discards roughly 90% of your XSS protection. Transition to cryptographic nonces (CSP.NONCE) and Django's json_script.
4. Specifying CSRF_TRUSTED_ORIGINS Without the Protocol Scheme
Since Django 4.0, configuring CSRF_TRUSTED_ORIGINS = ["example.com"] fails validation. Always provide the protocol scheme: ["https://example.com"].
5. Skipping python manage.py check --deploy in CI/CD Pipelines
Django includes a built-in automated check designed specifically for production deployments. Running this command should be a non-negotiable step in every CI/CD deployment pipeline.
Production-Ready Checklist: Security Settings for settings.py
Here is a hardened baseline configuration for production Django deployments:
# settings.py - Production Security Baseline
DEBUG = False
# HTTPS & Cookie Security
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = False # Must be False if client JS needs to read the token
CSRF_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_SAMESITE = "Lax"
# Trusted Origins for Cross-Origin CSRF
CSRF_TRUSTED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
# HTTP Strict Transport Security (HSTS)
SECURE_HSTS_SECONDS = 31536000 # 1 year
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Browser Defense Headers
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = "DENY"
# Reverse Proxy Configuration (e.g., behind NGINX or Kubernetes Ingress)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
Before deploying, run the automated security audit in your shell:
python manage.py check --deploy
This command flags unencrypted cookies, missing security headers, and active debug flags before your code reaches production. Looking for specialized support in auditing or designing enterprise-grade software? Discover our Cloud Security Services and bespoke Cloud Application Development.
Conclusion: Defense-in-Depth for Modern Django Applications
CSRF, CORS, and CSP may sound like an interchangeable alphabet soup of web security, but they address three fundamentally distinct threat boundaries:
- CSRF asks: Was this state-changing write request intentionally initiated by an authenticated user of my application?
- CORS asks: Is third-party browser JavaScript authorized to parse and process this API response?
- CSP asks: Which scripts, styles, and external connections is the browser permitted to execute on this web page?
Understanding these three controls empowers you to make informed architectural decisions and deploy resilient, enterprise-ready web applications. With native CSP support built into Django 6.0, the framework offers a complete, self-contained security toolkit that eliminates the need for legacy workarounds.
Frequently Asked Questions (FAQ)
What is the difference between CSRF, CORS, and CSP in Django?
CSRF prevents unauthorized state-changing write requests on behalf of an authenticated user. CORS is a browser security policy governing whether client-side JavaScript can read HTTP responses from another origin. CSP is an HTTP header restricting which scripts, styles, images, and resources the browser is permitted to load and execute.
What is the difference between CSRF_TRUSTED_ORIGINS and CORS_ALLOWED_ORIGINS?
CORS_ALLOWED_ORIGINS tells the browser who is allowed to read API responses (Access-Control-Allow-Origin). CSRF_TRUSTED_ORIGINS instructs Django on the server side to accept unsafe write requests (POST, PUT, DELETE) originating from that cross-origin domain. Both settings serve distinct security functions and cannot replace one another.
How does the built-in Content Security Policy (CSP) work in Django 6.0?
Starting with Django 6.0, CSP is natively integrated into the framework via django.middleware.csp.ContentSecurityPolicyMiddleware and configured through the SECURE_CSP (or SECURE_CSP_REPORT_ONLY) setting. External packages like django-csp are no longer strictly required for standard setups.
When does a Django REST Framework (DRF) API require CSRF protection?
CSRF protection is required whenever DRF uses SessionAuthentication with browser session cookies. If your API is purely stateless and relies on token or Bearer authentication passed in the HTTP Authorization header, CSRF protection is generally unnecessary.
Why is CORS_ALLOW_ALL_ORIGINS = True dangerous in production?
Setting CORS_ALLOW_ALL_ORIGINS = True allows any arbitrary third-party website to read sensitive data from your API via client-side JavaScript. When combined with credentials like cookies or auth headers, wildcard CORS completely undermines fundamental browser isolation mechanisms and exposes sensitive data.
How do I pass the Django CSRF token in AJAX and Fetch requests?
Extract the token from the cookie (by default csrftoken) via JavaScript and include it in the X-CSRFToken request header. Django's CsrfViewMiddleware automatically checks this header for non-safe HTTP methods.



















