Skip to content

🐛 Inconsistent X-Frame-Options header on cached responses and redirects #2799

Description

@igobranco

Summary

Richie CMS emits the X-Frame-Options HTTP header only on uncached 200 responses.
For cached CMS pages and language redirect (302) responses, the header is missing, resulting in inconsistent clickjacking protection.

This behavior is reproducible at the application level (bypassing ingress / reverse proxies) and appears to be caused by CMS caching and middleware short-circuiting.


Environment

  • Richie CMS (site factory–based deployment)
  • Django CMS
  • Gunicorn
  • No reverse proxy / ingress involved in the tests (direct pod access)

How to reproduce

Run the following commands inside the Richie container / pod, bypassing any proxy layers:

1. First request (uncached)

curl -v http://localhost:8000/en/

Response includes

X-Frame-Options: SAMEORIGIN

2. Subsequent request (served from CMS cache)

curl -v http://localhost:8000/en/

X-Frame-Options header is no longer present.

3. Language redirect

The root of the application that redirects to the language sub page also no includes the X-Frame-Options header.

curl -v http://localhost:8000/

Expected behavior

X-Frame-Options should be present consistently on:

  • cached responses
  • uncached responses
  • redirects (302)
  • all HTML responses emitted by Richie

This is required for reliable clickjacking protection.

Actual behavior

Header appears only once (first uncached request)
Cached responses bypass XFrameOptionsMiddleware
Redirects also bypass the middleware
Resulting behavior is inconsistent and fragile

Root cause analysis

In a Django CMS stack:

  • Cached CMS responses are replayed without re-running the full middleware chain
  • django.middleware.clickjacking.XFrameOptionsMiddleware is therefore skipped
  • Headers added late in the response phase are lost
  • Middleware order alone cannot fix this

This makes X-Frame-Options unreliable when relying solely on XFrameOptionsMiddleware.

Proposed fix (application-level, deterministic)

Ensure X-Frame-Options is enforced after CMS cache replay, e.g. in
richie.apps.core.cache.LimitBrowserCacheTTLHeaders, which already runs last during the response phase.

Minimal patch:

# Ensure X-Frame-Options is present even on cached responses
if not response.has_header("X-Frame-Options"):
    response["X-Frame-Options"] = getattr(
        settings, "X_FRAME_OPTIONS", "SAMEORIGIN"
    )

This guarantees:

  • cached responses
  • uncached responses
  • redirects

all leave the application with consistent framing protection, while remaining compatible with Django CMS (which requires SAMEORIGIN for previews/toolbars).

Temporary fix

On the web server layer: nginx, ingress nginx, or similar enforce the X-Frame-Options header value.

add_header X-Frame-Options "SAMEORIGIN" always;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions