Incident Report

The Hidden Danger of HttpOnly CSRF Cookies in Django

Published: May 19, 2026

1. The Incident: Locked Out by Security

Recently, based on a security vulnerability scan, an attempt was made to harden a web application by applying the HttpOnly=True flag to the CSRF cookie (setting CSRF_COOKIE_HTTPONLY = True in Django). The intention was straightforward: protect the CSRF token from Cross-Site Scripting (XSS) attacks by making it inaccessible to client-side scripts.

However, almost immediately after this configuration was deployed to the staging environment, a critical bug emerged: users were unable to log out or perform any state-changing actions. Every attempt to trigger a POST request resulted in a 403 Forbidden response with the error: "CSRF Failed: CSRF token missing or incorrect."

2. The Cause: Breaking the Double Submit Cookie Pattern

To understand why this happened, we must look at how modern web frameworks handle CSRF protection. Django, by default, relies on the Double Submit Cookie pattern.

Browser / Client Django Server 1. Set-Cookie (csrftoken) Cookie Store HttpOnly = True JavaScript (document.cookie) X BLOCKED 2. POST Request Header: X-CSRFToken: [MISSING] 403 Forbidden CSRF Token Check Failed

Django's Double Submit Cookie pattern works as follows:

  1. The server generates a random CSRF token and sends it as a cookie (e.g., csrftoken).
  2. The client-side application (React, Vue, etc.) reads this cookie value using JavaScript (document.cookie).
  3. The client then includes this value in a custom HTTP header (e.g., X-CSRFToken) for all non-GET requests.
  4. The server compares the token in the cookie with the token in the header. If they match, the request is authorized.

By setting HttpOnly=True, we effectively blocked JavaScript's access to that cookie. Consequently, the frontend could no longer read the token to populate the X-CSRFToken header. The server received the request with a missing header, failed the validation, and rightfully rejected the action.

3. The Blind Spot: Why QA Missed It

The pressing question was: Why wasn't this caught in Development or QA environments?

Investigation revealed a common pitfall: to simplify automated testing, the Dev and QA environments were using a custom middleware to bypass CSRF checks entirely.

# Custom middleware used only in Dev/QA
class DisableCSRF(MiddlewareMixin):
    def process_request(self, request):
        setattr(request, '_dont_enforce_csrf_checks', True)

This bypass created a massive blind spot. The security configuration was technically deployed, but its destructive side effects were invisible until it reached Staging, where the full security policy was active.

4. Lessons Learned and Next Steps

The configuration was immediately reverted. This incident highlighted several critical engineering principles:

  • Understand Your Security Patterns: Django's official documentation and OWASP guidelines explicitly mention that the CSRF cookie should not be HttpOnly if you are using the default AJAX/Double-Submit pattern.
  • Mirror Production Policies: Disabling core security mechanisms in lower environments creates dangerous discrepancies. Always aim to test with the same security posture as production.
  • Triaging Scanner Reports: Automated vulnerability scanners often flag missing HttpOnly flags on any cookie as a generic warning. It is the engineer's responsibility to triage these reports contextually.

Security hardening is essential, but it must be applied with a deep understanding of the underlying architectural patterns to avoid inadvertently breaking your own system.

G

Giri (Dong-gil Nam)

Backend Software Engineer

Passionate about building scalable systems and sharing technical insights. Specializing in JVM internals, distributed systems, and performance optimization.