A django csrf misconfiguration rarely announces itself. There's no stack trace, no failed build, no red banner in CI — just a form that quietly stops checking whether a request actually came from your site. Django has shipped CSRF protection on by default since Django 1.2 in May 2010, via CsrfViewMiddleware, and the framework's out-of-the-box defaults are genuinely solid. But defaults only protect what stays untouched, and production Django apps rarely stay untouched. Teams add CORS support for a mobile client, switch to token auth for an API, reorder middleware to fix an unrelated bug, or paste a CSRF_TRUSTED_ORIGINS wildcard from Stack Overflow to unblock a 2 a.m. deploy. Each change is small. Together, they're how a framework built to stop CWE-352 attacks ends up shipping session cookies that a third-party site can ride on.
What counts as a django csrf misconfiguration, and how common is it?
A django csrf misconfiguration is any deviation from Django's default CSRF posture — disabling CsrfViewMiddleware, exempting a state-changing view, trusting the wrong origins, or letting a token leak somewhere JavaScript can read it — and the underlying weakness class isn't rare. CSRF is tracked as CWE-352, and it lives inside OWASP's Broken Access Control category (A01:2021), which OWASP found present in 94% of the applications it tested for at least one form of broken access control, more than any other category in the Top 10. Django's own protection has existed for over 15 years, which is exactly why misconfigurations are almost never "Django doesn't protect against CSRF" — they're "someone turned the protection off, narrowed it, or routed around it," usually while solving a different problem under time pressure.
How does a CSRF token bypass django apps in practice?
A CSRF token bypass in django almost always comes through one of three doors: a view decorated with @csrf_exempt that shouldn't be, a CSRF_TRUSTED_ORIGINS entry broader than intended, or a frontend that stores the token in localStorage instead of letting Django's csrftoken cookie and hidden form field do the work. The @csrf_exempt case is the most common in the wild — webhook receivers for Stripe, GitHub, or an internal payment provider get exempted so the third party's POST doesn't 403, and the exemption stays in place long after anyone remembers to add the third party's own signature verification, leaving the endpoint open to anyone who finds the URL. The CSRF_TRUSTED_ORIGINS case got more common after Django 4.0 shipped in December 2021 and started requiring a scheme in that setting (https://example.com instead of a bare example.com), specifically to close a trust boundary that bare-domain entries had left too permissive. Teams that hit the startup error on upgrade often patched it fast with something like https://*.example.com without auditing which subdomains that pattern actually covers — re-opening, with one wildcard, the exact gap the framework had just tried to close.
What django session security settings get left on risky defaults?
The three settings that matter most for django session security are SESSION_COOKIE_SECURE, SESSION_COOKIE_SAMESITE, and SESSION_COOKIE_AGE, and Django's defaults for them are more permissive than most teams assume until an audit points it out. SESSION_COOKIE_SECURE defaults to False, meaning the session cookie will happily travel over plain HTTP unless a developer explicitly sets it to True — the same is true of CSRF_COOKIE_SECURE. SESSION_COOKIE_AGE defaults to 1,209,600 seconds, two weeks, so a session hijacked once stays valid for two weeks unless the app overrides it or sets SESSION_EXPIRE_AT_BROWSER_CLOSE (also False by default). SESSION_COOKIE_SAMESITE has defaulted to 'Lax' since Django 3.1 in August 2020, which is a real improvement over no SameSite attribute at all, but Lax still allows top-level GET navigations to carry the cookie — so a state-changing action wired to a GET request (an old anti-pattern, but one that still turns up in admin tooling and internal dashboards) is not protected by SameSite alone. None of these are bugs; they're defaults chosen for compatibility, and every one of them needs a deliberate decision in a production settings file, not silence.
Which django middleware security ordering mistakes disable CSRF checks?
Most django middleware security failures trace back to MIDDLEWARE ordering, because CsrfViewMiddleware has to sit after SessionMiddleware and Django's docs are explicit that moving it relative to other middleware can cause the CSRF check to run against the wrong request state or not run at all for requests handled upstream. The pattern shows up most often with django-cors-headers: CorsMiddleware has to be placed high in the stack to handle preflight requests, and a team debugging a CORS error will sometimes also loosen CORS_ALLOW_ALL_ORIGINS = True while CORS_ALLOW_CREDENTIALS = True stays set — a combination the library's own documentation warns against, because it lets any origin send authenticated, cookie-bearing requests to the app. A second common version is a custom middleware inserted above CsrfViewMiddleware that short-circuits certain paths with an early redirect or response, which quietly removes CSRF enforcement for exactly those paths without anyone touching csrf_exempt at all — which is what makes it hard to find in a code review that's only grepping for the decorator.
How have django csrf misconfigurations actually led to security incidents?
Django csrf misconfigurations tend to surface in pen tests and bug bounty reports rather than public breach disclosures, because the exploit path — a forged form submission from an attacker-controlled page — usually targets one account at a time rather than exfiltrating a database. The recurring pattern researchers flag is the webhook-exemption chain described above: an endpoint is marked @csrf_exempt to accept a third-party callback, the team never layers on the provider's HMAC signature check, and the endpoint ends up accepting state-changing requests — refunds processed, subscriptions changed, accounts flagged — from anyone who requests the URL, no forged token required. For teams under PCI DSS this is not just a code-quality issue: Requirement 6.5.9 in the PCI DSS Secure Coding standard names Cross-Site Request Forgery explicitly as a vulnerability class that secure development practices must address, so a csrf misconfiguration surfaced in an assessment becomes a documented compliance finding, not a GitHub issue that can sit in the backlog. The same logic applies under SOC 2's CC6.1 and CC6.6 criteria around logical access controls — an auditor asking how session and CSRF protections are enforced expects a specific, testable answer, not "Django handles that by default."
How Safeguard Helps
Safeguard treats a django csrf misconfiguration the same way it treats a vulnerable dependency: as a supply chain risk that should be caught before it ships, not discovered during an incident review. In practice that means scanning Django settings and middleware configuration as part of the same pipeline that inventories dependencies — flagging @csrf_exempt usage that lacks a corresponding signature check, CSRF_TRUSTED_ORIGINS or CORS_ALLOWED_ORIGINS wildcards, SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURE, or SESSION_COOKIE_SAMESITE values left on insecure defaults, and MIDDLEWARE orderings that place custom code ahead of CsrfViewMiddleware in ways that weaken enforcement. Findings map directly to the compliance frameworks that ask for them — PCI DSS 6.5.9, SOC 2 CC6.1/CC6.6 — so the evidence an auditor wants is already attached to the finding instead of being reconstructed after the fact. Because these checks run continuously against the actual codebase and its dependency graph, a regression introduced by a routine CORS fix or a middleware refactor gets flagged in the same pull request that introduced it, before it reaches production and long before it reaches an auditor's sample set.