Server-Side Cache Poisoning
detect, understand, remediate
Server-side cache poisoning lives inside the application boundary rather than at the CDN. It corrupts the object cache, the fragment cache, the full-page cache, the reverse-proxy cache, or the framework template cache that the origin reads, so a poisoned response is served to every subsequent user until the cache entry expires or is purged.
No credit card required. Free plan available forever.
What is server-side cache poisoning?
Server-side cache poisoning is the class of vulnerabilities where an attacker manipulates a cache that lives inside the application boundary so the poisoned response is served to subsequent users. The cache may be an object cache (Redis, Memcached, in-memory), a fragment cache (a per-view or per-partial template store), a full-page cache (a framework-rendered HTML store keyed by request), a reverse proxy cache positioned in front of the origin (Varnish, NGINX micro-cache, Apache mod_cache), or a framework template cache. The canonical CWE root is CWE-349 (Acceptance of Extraneous Untrusted Data With Trusted Data), with neighbouring CWE-444 (HTTP Request/Response Smuggling), CWE-565 (Reliance on Cookies without Validation), CWE-639 (Authorization Bypass Through User-Controlled Key), and CWE-444-style desync chains carrying the more specific failure shapes. OWASP groups these findings under A05:2021 Security Misconfiguration and, when the poisoned payload reflects into a user response, under A03:2021 Injection.
The category is distinct from edge-tier web cache poisoning and from web cache deception. Edge-tier poisoning targets the CDN or shared HTTP cache through unkeyed inputs at the proxy layer. Cache deception abuses path normalisation differences between the CDN and the origin to make the CDN store a personalised response under a URL it treats as static. Server-side cache poisoning targets the cache that the application itself owns or that sits in front of the origin within the security team operational scope. The blast radius is the same (every visitor receives the poisoned response until the entry expires), but the surface, the detection signal, and the remediation owner are different.
Server-side cache poisoning often pairs with HTTP request smuggling because a successful desync against the front-end proxy lets the attacker queue a poisoned request that the back-end serves to the next victim, and the poisoned response is stored in the application cache under the legitimate URL. The 2020 PortSwigger research (Practical Web Cache Poisoning) and the HTTP/2 desync research (2021 onwards) documented the chain in detail; modern advisories against frameworks like Drupal, WordPress (WP Super Cache, W3 Total Cache, LiteSpeed Cache), Magento Varnish, Rails fragment cache, Symfony HTTP cache, and Django cache middleware reference variants of this pattern. Pairing the finding with host header injection is also common when the application uses the Host header to generate absolute URLs that land inside the cache entry.
Where the server-side cache lives
A defensible remediation plan starts by naming the cache tier the finding sits on. The five tiers below are the surfaces that show up in real engagement evidence; a finding that does not name the tier produces a closure the auditor cannot read and a regression the next scan cannot pair.
Object cache (Redis, Memcached, in-memory)
A key-value store the application reads before computing a response. WordPress object cache, Drupal cache backends, Django cache framework, Rails Rails.cache, Symfony Cache Component, and Spring Cache all fit here. The poisoning vector is a low-cardinality key that lets an attacker set the value an authenticated reader retrieves.
Fragment cache
A per-view or per-partial template store. The framework renders a fragment, stores it under a key derived from the request or the model, and serves the stored fragment on subsequent renders. Rails fragment cache, Symfony fragment subrequest cache, and ASP.NET output cache fragments all fit here. Poisoning happens when the fragment key omits user context.
Full-page cache
A framework or plugin-level full-page HTML store. WP Super Cache, W3 Total Cache, LiteSpeed Cache, Drupal Internal Page Cache, Magento Varnish full-page cache, and Symfony HTTP cache live here. Poisoning happens when the cache key drifts from the inputs that materially change the rendered HTML.
Reverse proxy cache
An NGINX micro-cache, Apache mod_cache store, Varnish backend cache, or HAProxy cache sitting between the application server and the CDN. The cache typically sits inside the security team operational boundary rather than at the CDN vendor tier. Poisoning happens through unkeyed header reflection or HTTP/2 desync against this layer.
Framework template and config cache
A compiled template store, a config cache, or a route cache that the framework reads on warm start. Laravel route and config cache, Symfony container cache, ASP.NET view cache, and Twig compiled templates fit here. Poisoning is rarer but high-impact: a writable cache directory plus a path traversal can compile attacker code.
HTTP/2 desync into back-end cache
A request smuggling chain that exploits CL.TE, TE.CL, H2.CL, or H2.TE discrepancies between the front-end proxy and the back-end origin. The smuggled request rewrites a queued response in the front-end socket, the back-end stores the rewritten response under the legitimate URL, and the cache serves the poisoned entry. Modern HTTP/2 frontends amplify the attack surface.
How server-side cache poisoning works
Identify the cache tier
The attacker fingerprints which cache the origin actually reads. Response headers (X-Cache, Age, Via, X-Drupal-Cache, X-Magento-Tags, X-Powered-By), behavioural timing (cold versus warm latency), and known framework signatures reveal the cache surface. A finding cannot be exploited consistently until the correct tier is identified.
Map the cache key
The attacker enumerates which inputs the cache key includes. Path, query string, request method, Vary headers, cookies, and selected request headers may or may not be part of the key. The Vary response header is a hint but is often inaccurate at the server tier compared to what the cache backend actually keys on.
Find an unkeyed input that influences the response
The attacker injects values into inputs that are not in the cache key but still affect the rendered response. Host header, X-Forwarded-Host, X-Original-URL, X-Rewrite-URL, X-Forwarded-Proto, and fat-GET bodies are common vectors. A reflected payload that survives cache write is the proof of exploitability.
Persist the poisoned response
The attacker submits the poisoned request, waits for the cache to absorb the response, then confirms from a clean network that subsequent requests for the legitimate URL return the poisoned content. The poisoning persists until the entry expires (TTL), is invalidated by a write, or is manually purged.
Common causes
Cache key omits user context
A fragment or object cache key keyed only on the resource identifier ignores the authenticated user. The first user request hydrates the cache with their personalised data; subsequent requests retrieve it. Session bleed, profile leak, and authorisation bypass all surface from this pattern. The fix is to bind the cache key to user identity (user id, tenant id, role) where the response varies by user.
Cache read before authorisation check
The controller reads the cache before running the authorisation middleware. A cached response built for a privileged context is returned to an unprivileged caller because the cache lookup short-circuits the access check. The fix is to run authorisation first and treat the cache as an answer for an already-authorised request.
Unkeyed Host or X-Forwarded headers reflected in response
The origin reads Host, X-Forwarded-Host, X-Forwarded-Proto, or X-Original-URL to build absolute URLs (canonical link, password reset link, Open Graph meta, asset URLs) but the cache key does not include those headers. Attacker-controlled values land in the cached response. The fix is to strip these headers at the edge, normalise to a trusted value, and include them in the key.
Default framework full-page cache on authenticated routes
WP Super Cache, W3 Total Cache, LiteSpeed Cache, Drupal Internal Page Cache, Symfony HTTP cache, and Magento Varnish ship with default rules that may cache authenticated routes when the cookie naming convention is non-standard. The cache stores a personalised page under a public key, and the next anonymous visitor sees it.
Front-end and back-end disagree on request framing
A CL.TE, TE.CL, H2.CL, or H2.TE discrepancy between the front-end proxy and the back-end origin lets a smuggled request reach the back-end out of order. The back-end serves the smuggled request, the front-end stores the response in the legitimate request socket, and the cache absorbs the poisoned entry. HTTP/2 to HTTP/1.1 downgrades are a frequent source.
Fat-GET requests with bodies
Some frameworks process GET request bodies (Spring with allow-body GET, certain Express middleware, custom routing). The cache backend ignores the body when computing the key, but the origin processes it. An attacker injects the payload in the body that is invisible to the cache key.
Cache tag invalidation is incomplete
Frameworks (Drupal cache tags, Symfony tag invalidation, Magento Varnish tags) rely on explicit invalidation on write. A missing tag, a typo in the tag list, or an external update path that bypasses the application skips invalidation. A poisoned entry persists past the model write that should have purged it.
Stale-while-revalidate without cache key hygiene
The application uses stale-while-revalidate or stale-if-error semantics to serve old responses while refreshing in the background. If the key is weak, the poisoned entry is served as stale during the entire revalidation window even after the input source is fixed.
How to detect server-side cache poisoning
Detection runs across two surfaces. Active testing produces signals an external scanner, an authenticated scanner, and a code scanner can read against the application implementation. Configuration review against the actual cache backend (Redis keys, Varnish VCL, NGINX cache zones, framework cache config) reads the static evidence the scanner cannot reach. A defensible posture runs both halves and records the finding on the canonical engagement record.
Active testing signals
- SecPortal external scanning fingerprints internal cache tiers (Varnish, NGINX micro-cache, framework page caches) through response headers and behavioural timing, then probes unkeyed header reflection against the origin layer rather than the CDN tier alone. The finding lands with the affected route, the reflected header, and the cache fingerprint as evidence.
- Authenticated scanning probes authenticated routes for object-cache and fragment-cache reuse across users, sending requests from two distinct authenticated sessions and comparing the responses for personalised content leakage. A divergence between user-scoped data and cached response is the proof.
- Code scanning runs Semgrep rule packs against connected repositories for low-cardinality cache keys (Rails cache(model) without user context, Django cache.get(key) where the key drops session identity, WordPress wp_cache_set with a public group on authenticated data), cache reads positioned before authorisation middleware, and framework full-page cache enabled on routes that read user session.
- Findings management captures the CWE-349 mapping, the OWASP A05 category, the affected cache tier, the cache key shape, the poisoned input, the evidence (request/response captures, proof-of-concept), and a CVSS 3.1 vector calibrated for the blast radius rather than the generic base score. The record carries to retesting unchanged.
Configuration review signals
- Inspect the Varnish VCL or NGINX cache configuration for the actual cache key. Compare the configured key against the headers and parameters the origin reads. Any input the origin uses that does not appear in the key is a candidate finding.
- Audit framework cache configuration: WordPress object cache drop-in, Drupal cache backends in services.yml, Symfony framework.cache.pools, Rails config.action_controller.perform_caching, ASP.NET output cache profiles, Django CACHES setting. Each cache backend should declare its key strategy and what user context it includes.
- Review cache tag and invalidation paths. Every write that should invalidate cached data should reference a tag or key list. A model update that does not invalidate the cached fragment is a regression vector and a finding worth recording even before exploitation.
- Test for HTTP/2 desync paths with controlled probes against the front-end proxy. PortSwigger HTTP Request Smuggler, Burp HTTP Smuggler probes, and h2c smuggling checks reveal CL.TE, TE.CL, H2.CL, and H2.TE conditions. A confirmed desync against a cache-fronted route is a critical-severity finding.
- Subscribe to vendor security advisories for the cache layer: Drupal Security Team, WordPress core and plugin advisories (WP Super Cache, W3 Total Cache, LiteSpeed Cache), Magento Varnish, Symfony, Rails, Django, Spring, and Varnish itself. A cache plugin CVE is a same-day finding on every engagement that touches the affected version.
How to remediate server-side cache poisoning
Bind every cache key to the user context the response depends on
If the response varies by authenticated user, include user id, tenant id, role, or session-derived identifier in the cache key. If the response varies by feature flag, include the flag value. If the response varies by locale, include the locale code. A weak key is the single most common cause of server-side cache poisoning; tightening the key removes the entire class of bugs on the affected route.
Move authorisation checks before cache lookups
The controller should run the authorisation middleware first, then look up the cache. The cache should answer the question "what is the response for an already-authorised request to this route under this key" rather than acting as the front line of access control. Frameworks that allow inverting this order (Symfony cache before security firewall, Rails before_action ordering) should be audited explicitly.
Strip and normalise edge-injected headers at the perimeter
X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Scheme, X-Original-URL, X-Rewrite-URL, and Host should be normalised at the front-end proxy or load balancer to a trusted value before reaching the origin. The origin should never trust attacker-controlled header values for absolute URL generation, canonical link rendering, or asset path construction.
Disable framework full-page cache on authenticated routes
Configure WP Super Cache, W3 Total Cache, LiteSpeed Cache, Drupal Internal Page Cache, Magento Varnish, Symfony HTTP cache, and equivalent plugins to skip authenticated requests by checking the session cookie name explicitly. Add unit tests that fail if an authenticated response is ever stored under a public cache key. Treat cache plugin defaults as untrusted on every release.
Close request smuggling paths between front-end and back-end
Validate that the front-end proxy and the back-end origin agree on request framing under HTTP/1.1, HTTP/2, and HTTP/2 to HTTP/1.1 downgrade. Disable Transfer-Encoding from the back-end where the front-end does not pass it, reject ambiguous Content-Length and Transfer-Encoding combinations, and disable HTTP/2 downgrade against origins that cannot frame correctly. The PortSwigger HTTP Desync research is the practical guide.
Add cache tag invalidation tests on every write path
Every write that touches a model whose cached representation is served should fire a tag invalidation. Add integration tests that mutate the model, retrieve the response, and assert the cache reflects the new state. A missing tag is a regression vector even if it has not been exploited; the test makes the regression observable.
Set Cache-Control: private, no-store on personalised responses
Authenticated and personalised responses should carry Cache-Control: private, no-store explicitly. The header is advisory and not enforced by every cache tier, but it is the documented signal that downstream caches and intermediaries respect, and it makes the intent reviewable in audit evidence.
Purge the cache after a confirmed poisoning finding
The first remediation step on a confirmed finding is to purge the affected cache entry. Document the purge in the activity log, hold the entry purged through the fix window, and only restore caching when the structural fix is shipped. Closure on a fresh retest against the original poisoned key is the only durable evidence the fix worked.
Verify closure with a directed retest
A server-side cache poisoning finding moves to closed only when a directed retest reproduces the original exploit conditions and the response no longer leaks personalised data, no longer reflects attacker-controlled headers, and no longer survives a fresh purge. Closure on documentation alone is the largest single contributor to reopen rate on this finding class.
How SecPortal records and tracks server-side cache poisoning findings
SecPortal is not a cache plugin, is not a WAF, and does not enforce cache policy on your application. It is the workspace where every cache-tier finding lands on the engagement record alongside the source-level findings, the external attack-surface findings, and the authenticated DAST findings so severity, ownership, evidence, fix, and retest stay on one canonical record per affected route and cache tier.
Run external scanning across the verified perimeter
External scanning fingerprints reverse proxy and CDN tiers through response headers and behavioural timing, then probes unkeyed header reflection against the origin layer. Findings land on the engagement record with the affected route, the reflected header, and the cache fingerprint as evidence.
Run authenticated scanning behind login
Authenticated scanning probes for object-cache and fragment-cache reuse across users by issuing requests from two distinct authenticated sessions and comparing the responses for personalised content leakage. The divergence is the proof and lands on the engagement record with both response captures.
Run code scanning across connected repositories
Code scanning executes Semgrep rule packs across connected GitHub, GitLab, and Bitbucket repositories for low-cardinality cache keys, cache reads positioned before authorisation middleware, full-page cache enabled on session-reading routes, and HTTP/2 desync risks at the application layer. Findings carry the file path, line number, and matched rule on the engagement record.
Record findings with the CWE and OWASP mapping
Findings management captures CWE-349 (and CWE-444 where smuggling is in scope), the OWASP A05:2021 mapping, the affected cache tier (object, fragment, full-page, reverse proxy, HTTP/2 desync), the cache key shape, the poisoned input vector, and a CVSS 3.1 vector calibrated for the blast radius rather than the generic base score.
Import third-party cache findings via bulk import
Bulk finding import lets you bring in cache-related findings from a third-party pentest, a Burp Suite Pro scan, a manual review, a Drupal Security Team advisory, a WordPress plugin advisory, or a Magento Varnish CVE export. The imported finding lands on the engagement record under the same canonical record shape with no parallel backlog.
Capture cache-tier exceptions on the finding record
When a cache poisoning path cannot be remediated immediately (a legacy plugin without an upgrade path, a third-party Varnish configuration outside the security team operational scope, a managed CDN tier the team does not own), the exception lives on the finding through the override pattern with named owner, compensating controls, residual-risk rationale, review cadence, and expiry rather than in a meeting note.
Verify closure through retesting workflows
The retesting workflow pairs the retest to the original finding so closure means a directed retest against the original poisoned input, the original cache tier, and the original cache key reproduces the conditions and shows the response no longer leaks personalised data. The verified_at and resolved_at timestamps preserve the audit chain.
Map findings against multi-framework compliance
Compliance tracking maps server-side cache poisoning against OWASP A05:2021, PCI DSS Requirement 6.4 (Public-Facing Web Application Protection), ISO 27001 Annex A.8.9 (Configuration Management), SOC 2 CC7.1 (System Monitoring and Detection), NIST SP 800-53 SI-7 (Software, Firmware, and Information Integrity), NIST CSF 2.0 PR.PS, NIS2 Article 21, and CIS Controls Control 16 (Application Software Security) in parallel.
Capture the activity log as the workspace audit chain
Activity log records every workspace decision (engagement scope, finding triage, severity override, status transition, exception approval, purge action, retest closure) with the acting user and timestamp, exports to CSV, and gives the GRC team an audit-grade record of how the cache poisoning finding moved through the programme.
What SecPortal does not do
Honesty on capability matters when the topic is cache hygiene at the origin tier. SecPortal does not run a managed Varnish or NGINX cache for your application, does not own your CDN configuration, does not push cache invalidation against Cloudflare, Fastly, Akamai, AWS CloudFront, Azure Front Door, or Google Cloud CDN APIs, does not deploy a WAF in front of your application, does not act as a reverse proxy on the request path, and does not ship packaged push connectors into Jira, ServiceNow, Slack, Teams, PagerDuty, SIEM, SOAR, GRC, ticketing, or vendor cache APIs. Programmes that need real-time edge cache control, managed Varnish tier operation, or CDN policy enforcement run a dedicated cache and edge stack, and land the resulting cache poisoning findings on the SecPortal engagement record through bulk finding import. The platform value is the consolidated record where every cache-tier finding (whether it came from native external scanning, authenticated scanning, code scanning, bulk import from an outside pentest, a Drupal or WordPress security advisory, or a manual configuration review against Varnish VCL) lives alongside the rest of the security backlog with the same lifecycle, the same role-based access control, the same activity log, and the same evidence trail.
Related vulnerabilities and recommended reading
Server-side cache poisoning sits in the wider misconfiguration and integrity cluster. The pages below cover adjacent finding shapes, the frameworks that map control evidence against cache configuration, and the programme workflows that hold the cache backlog across detect, triage, prioritise, route, remediate, and verify.
- Edge-tier web cache poisoning the sibling category that targets the CDN and shared HTTP cache through unkeyed inputs at the proxy layer.
- Web cache deception the path-handling abuse where the edge cache stores an authenticated response under a URL it treats as static.
- HTTP request smuggling the desync chain that enables a smuggled poisoned request to land in the back-end cache under the legitimate URL.
- Host header injection the input vector most often used to land attacker-controlled values inside the server-side cache entry.
- Security misconfiguration the parent OWASP A05 category that captures cache-key drift and framework default-cache mistakes.
- Business logic flaws the sibling cluster where cache reuse across users surfaces as authorisation bypass through a cache-key omission.
- Cross-site scripting (XSS) the high-impact payload class that cache poisoning often persists across every visitor through a stored reflection.
- Information disclosure the finding shape that surfaces when a cached personalised response leaks to anonymous viewers.
- OWASP ASVS Chapter V11 Configuration and V12 Communication carry the verification points the cache finding reads against.
- OWASP WSTG the testing guide section on cache-control and reverse proxy behaviour that anchors the active probe checklist.
- ISO/IEC 27001 Annex A.8.9 (Configuration Management) and A.8.25 (Secure Development Life Cycle) are the ISO mappings for cache configuration evidence.
- PCI DSS Requirement 6.4 covers public-facing web application protection where the cache tier is part of the attack surface.
- NIST CSF 2.0 the PR.PS Platform Security and DE.AE Adverse Event Analysis categories map cache hygiene to outcomes.
- External scanning the feature page covering external probe modules that fingerprint cache tiers and detect unkeyed header reflection at the origin.
- Authenticated scanning the feature page covering authenticated probes that surface object-cache and fragment-cache reuse across user sessions.
- Finding overrides the override register where cache-tier exceptions live with named owner, compensating controls, and review cadence.
- Remediation tracking the workflow that carries cache-tier findings through purge, structural fix, and retested closure on one record.
- Security leadership reporting the workflow that carries the cache posture into the leadership read and the audit committee narrative.
- Security findings deduplication guide the practitioner guide for handling near-duplicate cache findings across edge and origin tiers.
- SecPortal for internal security teams the audience overview for the internal teams that own cache hygiene across the application portfolio.
Compliance impact
OWASP Top 10
A05:2021 Security Misconfiguration
PCI DSS
Req. 6.4 Public-Facing Web Application Protection
ISO/IEC 27001
A.8.9 Configuration Management, A.8.25 Secure SDLC
SOC 2
CC7.1 System Monitoring and Detection
NIST 800-53
SI-7 Software, Firmware, and Information Integrity
NIST CSF 2.0
PR.PS Platform Security, DE.AE Adverse Event Analysis
NIS2 Directive
Article 21 Cybersecurity risk-management measures
CIS Controls
Control 16 Application Software Security
Related features
Vulnerability scanning tools that map your attack surface
Test web apps behind the login
Find vulnerabilities before they ship
Vulnerability management software that tracks every finding
Finding overrides that survive every scan cycle
Every action recorded across the workspace
Compliance tracking without a full GRC platform
Verify fixes and track reopens on the same finding record
Monitor continuously catch regressions early
Track server-side cache poisoning on one engagement record
SecPortal pairs the external scan, the authenticated scan, and the code scanner with one findings record per cache-tier finding, with CVSS 3.1 severity, framework mapping, retest pairing, and an append-only activity log. Start scanning for free.
No credit card required. Free plan available forever.