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;
Summary
Richie CMS emits the
X-Frame-OptionsHTTP 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
How to reproduce
Run the following commands inside the Richie container / pod, bypassing any proxy layers:
1. First request (uncached)
Response includes
2. Subsequent request (served from CMS cache)
X-Frame-Optionsheader 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-Optionsheader.Expected behavior
X-Frame-Options should be present consistently on:
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:
django.middleware.clickjacking.XFrameOptionsMiddlewareis therefore skippedThis makes X-Frame-Options unreliable when relying solely on
XFrameOptionsMiddleware.Proposed fix (application-level, deterministic)
Ensure
X-Frame-Optionsis enforced after CMS cache replay, e.g. inrichie.apps.core.cache.LimitBrowserCacheTTLHeaders, which already runs last during the response phase.Minimal patch:
This guarantees:
all leave the application with consistent framing protection, while remaining compatible with Django CMS (which requires
SAMEORIGINfor previews/toolbars).Temporary fix
On the web server layer: nginx, ingress nginx, or similar enforce the
X-Frame-Optionsheader value.