Yaamlabs
Vulnerabilities

Cache key injection: when two requests share one cache entry

YesWeHack shows how Nginx cache keys built without separators let attackers read restricted pages, cache 404s and plant XSS. How to check yours.

By Yaali. October 3, 2026, 6 min read, Vulnerabilities, Resilience.

Cover illustration of two glowing keys merging into one above stacked server cache drawers, with the Yaamlabs logo and the text: Cache key injection turns one request into another, /h + ome*/*, collides with /home

YesWeHack researcher Alex Brumen has published a technique he calls cache key injection. It targets the string a web cache uses to decide whether two requests are "the same". When that string, the cache key, is built by gluing request values together with nothing between them, an attacker can craft a different request that produces exactly the same key as a legitimate one. Using Nginx, he shows this leads to reading pages protected by an access rule, replacing real pages with error responses, and in one setup to stored cross-site scripting (XSS).

There is no CVE and no patch, because nothing in Nginx is broken. The weakness sits in configuration: any proxy_cache_key (or similar) line that you or a previous admin extended with $host, a request header or a cookie, and wrote without separators. If you run Nginx as a caching reverse proxy, including behind a CDN such as Cloudflare, it takes a few minutes to check whether yours is exploitable.

How cache key injection works: a legitimate request for /home with Accept */* and an attacker request for /h with Accept ome*/* both produce the cache key ending /home*/*, so the cache serves the attacker's stored 404 to every later visitor

How it works

A shared cache stores a response under a key and hands it to every later request that produces the same key. Nginx's default is proxy_cache_key $scheme$proxy_host$request_uri;: the scheme (http or https), the upstream name from your own proxy_pass line, and the path with its query string. An outside client can only vary the last of those, and every path starts with /, so the default is hard to collide.

Trouble starts when the key gets extra parts. A site that returns different content depending on the Accept header might use proxy_cache_key "$scheme$host$request_uri$http_accept";. Nginx joins the four values into one string with nothing marking where one ends and the next begins. A request for /home with Accept: */* gives a key ending in /home*/*. So does a request for /h with Accept: ome*/*. The cache has no way to tell them apart.

Brumen builds three attacks on that ambiguity:

  • Reading a restricted page. His test server only lets 127.0.0.1 reach /admin, and caches everything for 30 seconds. After an admin loads /admin with Accept: */*, an attacker requests /ad with Accept: min*/*. Nginx checks its access rule against /ad, which is not restricted, then finds the cached /admin page under the same key and returns it. This is web cache deception with no victim click: the admin only has to use the page normally.
  • Cache-poisoned denial of service (CPDoS) works the other way round. The attacker requests /h with Accept: ome*/* before anyone else fetches /home. The origin answers 404, Nginx stores it under the /home*/* key, and real visitors get the 404 until the entry expires.
  • The stored XSS case needs more conditions. If the key starts with $scheme$host, the server accepts any Host header, ports 80 and 443 serve the same site without redirecting HTTP to HTTPS, and the page reflects the Host value into a script src attribute, then a plain HTTP request with Host: sdummywebsite.localhost gives the key httpsdummywebsite.localhost/. A normal HTTPS request for dummywebsite.localhost gives the same string. The response cached for the attacker loads its script from a host name the attacker chose, and every HTTPS visitor gets it.

Hashing the final key does not help, Brumen notes, because identical strings give identical hashes. The boundary between parts has to survive into the key itself.

Getting past the CDN

Many Nginx caches sit behind an edge CDN. Brumen shows how an attacker steers around it. With Origin Cache Control turned on, which Cloudflare's documentation says is the default on Free, Pro and Business plans, a request carrying an Authorization header is only cached at the edge if the response says Cache-Control: public, s-maxage or must-revalidate. Otherwise Cloudflare marks it CF-Cache-Status: BYPASS and forwards it to the origin.

Nginx has no such rule unless you add one. So the attacker adds any Authorization header to the colliding request, Cloudflare passes it straight through, and Nginx caches the poisoned response under the shared key. When the edge copy of the page expires, or for a page the edge never cached, Cloudflare fetches it from Nginx and receives the poisoned version. Cloudflare's own documentation adds that on Enterprise plans with Origin Cache Control turned off, Authorization does not by itself prevent edge caching.

Who is exposed

The research is a technique write-up, not a report of attacks in the wild. YesWeHack calls the class underestimated and says the work only scratches the surface. You are exposed if all of these hold:

  1. Nginx caches responses (proxy_cache is set somewhere in the config).
  2. The cache key joins two or more values that a client can influence, such as $host, $request_uri, $uri, $args, any $http_* header or any $cookie_* value, with no separator between them.
  3. The cached content differs in a way that matters: an access rule on some paths, error responses that get cached, or a Host value reflected in the page.

What to do

Checklist for Nginx caches: find every cache key, add separators, keep authenticated requests out of the cache, lock down Host and HTTP, align CDN and origin rules, then purge and test

1. Find every key

Dump the full running config and search it, so included files are covered:

nginx -T 2>/dev/null | grep -nE '(proxy|fastcgi|uwsgi|scgi)_cache_key'

Brumen's examples use proxy_cache_key. The FastCGI, uWSGI and SCGI cache keys are built the same way from variables, so review them with the same eye. A missing proxy_cache_key means the default, which is safe on this point.

2. Add separators

Brumen's fix is a delimiter between every part:

proxy_cache_key "$scheme|$host|$request_uri|$http_accept";

Then remove what does not need to be in the key. If the backend only serves one content type, $http_accept can go.

3. Keep authenticated requests out of the shared cache

Nginx's documentation gives the pattern itself:

proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization;

Do the same for session cookies ($cookie_<name>) on any location that serves per-user or restricted content. A page behind allow 127.0.0.1; deny all; or a login should not sit in a cache that other clients can reach.

4. Close the scheme and Host trick

Redirect all port 80 traffic to HTTPS with a separate server block that only returns 301. Add a default server block that rejects unknown Host values (for example return 444;), so only your real host names reach the cached site.

5. Line up the CDN and origin rules

Whatever the edge refuses to cache (requests with Authorization, specific cookies, certain paths), the origin cache should refuse too. Write both lists down and compare them.

6. Purge and check

After changing a key, clear the cache directory set in proxy_cache_path so no old entries linger. To see whether someone has tried this, add $upstream_cache_status and $http_accept to your log_format, then look for requests that receive a HIT on a path that does not exist on the backend, or Accept values that look like the tail of a path (ome*/*, min*/*). Brumen's black-box method splits a test value across two keyed parts and checks whether the response comes from the cache; run the same test on staging to confirm your fix.

The wider lesson

Cache keys tend to be edited once, to fix a content negotiation bug or a multi-site setup, and never read again. They deserve the same review as any parser that combines untrusted input: every place two client-controlled values meet needs an unambiguous boundary, and every layer of caching needs the same idea of what must never be shared.

Our web penetration testing covers cache poisoning and cache deception against the real stack, CDN and origin together, and our safeguarding and hardening work reviews Nginx and CDN configuration line by line. Open the chat and Yaali, our AI agent, will pass your question to the engineer who would do the work.


Sources: YesWeHack research: Cache key injection, GBHackers, Cyber Security News, Cyberpress, Nginx proxy module documentation, Cloudflare cache responses documentation.

Read next

Back to the blog, or tell us about your system in the chat. Yaali, our AI agent, answers first and brings in an engineer.