
A reverse proxy vulnerability is a request-smuggling or cache bug in front of every origin you hide behind it.
If Varnish or another proxy misreads a header, the next hop may see a different request than the client sent. That is how one CVE becomes every site on the box.
The usual mistake is patching the app and leaving the proxy on last year’s package because ‘it only caches static files.’ The proxy still parses HTTP.
This page is the Varnish advisories that matter, what CWE-444 looks like on the wire, and the version check you run on the edge.
CVE-2025-47905 is CWE-444. NVD published it on 13 May 2025. The first-party advisory is VSV00016, dated 12 May 2025: Varnish Cache through 7.7.0 treated some HTTP/1 chunk boundaries as valid when the CRLF that RFC 9112 requires was missing. That is a framing disagreement, not a new product class. The same class still poisons a cache when Varnish and the origin disagree on where one request ends.
The control is a parser that agrees with the origin, a cache key that does not include a session, and a package that already contains VSV00016 and VSV00019. Pair the host with the Ubuntu host guide. Pair the response headers the origin must send with the headers guide. If the app issues a session cookie, read session management before you decide that object is public.
VSV00016, 12 May 2025: HTTP/1 framing still matters
Varnish advisory and the NVD page. VSV00016 is a client-side desync on HTTP/1 chunked bodies. The project rated it medium. Fixed releases on the same day were 7.7.1, 7.6.3, and 6.0.14 LTS. Varnish Enterprise shipped 6.0.13r14 on 9 April 2025. If varnishd -V still prints 7.7.0 or 6.0.13, you are on the affected line. Upgrade, then restart the worker so the new binary is the one listening.
Varnish has to agree with the origin on where the HTTP/1 request ends, then refuse to store a body that still belongs to a logged-in person.
SecureCoding
The class is older than that CVE. HTTP/1 reuses a TCP connection. The proxy and the origin each decide where message N ends and message N+1 begins. When those two decisions diverge, the next response on the wire can be stored under the wrong URL. That stored object is the poison. You do not need a demo to defend against it. You need one framing rule at the edge, and you need to refuse to store a response that belongs to a logged-in person.
The project’s own mitigation for an unpatched 7.7.0, copied from the advisory so you can recognize it, is fail-closed on chunked client bodies:
# Only if you cannot upgrade yet. Prefer 7.7.1 / 6.0.14 / later.
# VSV00016 mitigation from varnish-cache.org/security/VSV00016.html
vcl 4.1;
sub vcl_recv {
if (req.http.transfer-encoding ~ "(?i)chunked") {
return (fail);
}
}
That snippet will also fail some honest clients. The advisory says so. Treat it as a bridge, not a policy. The durable fix is the patched varnishd.
Normalize the request before you hash it
Two speakers that see different hosts, different ports, or two length headers will fetch or store different objects. Your job at the edge is to make one request out of whatever arrived, or to drop it. Do not forward ambiguity.
RFC 9112 says a sender must not produce both Content-Length and Transfer-Encoding on one message. A cache that accepts both and picks a winner is choosing a frame the origin may not choose. Fail it.
# /etc/varnish/normalize.vcl
vcl 4.1;
import std;
sub vcl_recv {
if (req.http.Content-Length && req.http.Transfer-Encoding) {
return (synth(400, "ambiguous length"));
}
# One Host, no leftover port, no hop-by-hop leftovers on the hash.
if (!req.http.host) {
return (synth(400, "missing host"));
}
set req.http.host = std.tolower(req.http.host);
if (req.http.host ~ ":[0-9]+$") {
set req.http.host = regsub(req.http.host, ":[0-9]+$", "");
}
unset req.http.Keep-Alive;
unset req.http.Proxy-Authenticate;
unset req.http.Proxy-Authorization;
unset req.http.TE;
unset req.http.Trailer;
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
}
Identifiers stay normalize.vcl, req.http.host, and ambiguous length. std.tolower needs import std. If your package is older than the std that ships with current Varnish, upgrade the package rather than inventing a regex. 0 built-in VCL layout from the project’s builtin.vcl.
Hop-by-hop names do not belong on the cache key and they do not belong on the backend fetch. Connection is the one you may set later to close when you want the origin connection retired. Do not unset it in a later subroutine if you set it on purpose. The secure coding checklist is the rest of the app. Here we only cover the object store in front of it.
Built-in VCL already refuses Cookie and Authorization
Varnish 8.0.2 built-in VCL reference. After your vcl_recv returns without a decision, the built-in path still runs. vcl_req_authorization returns pass when req.http.Authorization is set. vcl_req_cookie returns pass when req.http.Cookie is set. On the backend side, a Set-Cookie response is marked uncacheable. Those three lines are the product’s default for “this is a person, not a public page.”
The usual own-goal is to empty the session cookie so the built-in check no longer fires, then store the body that still contains that person’s dashboard. A tracking cookie you do not need can be stripped. A session cookie that changes the HTML cannot. If any cookie remains and the page is personalized, pass is the correct outcome. If you stripped everything, unset req.http.Cookie so the built-in check sees an empty header, not a leftover SESSIONID=.
# /etc/varnish/default.vcl
vcl 4.1;
import cookie;
backend origin {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
if (req.http.Authorization) {
return (pass);
}
if (req.http.Cookie) {
cookie.parse(req.http.Cookie);
cookie.filter("_ga,_gid,_fbp");
set req.http.Cookie = cookie.get_string();
if (req.http.Cookie == "") {
unset req.http.Cookie;
} else {
return (pass);
}
}
if (req.url ~ "^/account" || req.url ~ "^/admin") {
return (pass);
}
}
Identifiers stay origin, cookie.filter, /account, and /admin. vmod_cookie ships with current Varnish. If the module is missing, do not invent a regex that drops Cookie on every URL. return (pass) on the whole site is safer than a half strip.
VCL you write: pass, hash, Vary
The cache key is vcl_hash. Built-in hashing already includes the host and the URL. Add a header only when the origin actually varies on it and you have decided that variant is public. Do not hash Cookie. Hashing a session id stores one object per person under a key anyone who can guess or replay that id might hit, or it fragments the cache into useless singles. Either way you have left the “public page” model.
sub vcl_hash {
hash_data(req.url);
hash_data(req.http.host);
if (req.http.Accept-Encoding ~ "gzip") {
hash_data("gzip");
}
}
sub vcl_backend_response {
if (beresp.http.Set-Cookie || beresp.http.Authorization) {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
if (beresp.http.Cache-Control ~ "(?i)private|no-store") {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
if (bereq.http.Cookie || bereq.http.Authorization) {
set beresp.uncacheable = true;
set beresp.ttl = 0s;
return (deliver);
}
}
The origin must send Cache-Control: private or no-store on anything that includes a name, an email, a CSRF token, or a cart. Varnish honoring that header is not optional flavor. It is the second belt after the request-side pass. If the app sets a session cookie, that cookie needs the flags on the session page. This file does not replace that work.
Surrogate keys and ban lurker jobs are how you drop a URL after a publish. They are not a substitute for “never stored.” A ban that runs after a leak is incident response.
The 2026 HTTP/2 flag is a different door
CVE-2026-50052 is VSV00019, disclosed 18 May 2026. NVD’s text is a backend desync on HTTP/2 parsing, usable for a poisoned object or an auth skip, and only if the feature parameter contains +http2. HTTP/2 is off in the default varnishd. Fixed lines the project named the same day include 8.0.2, 6.0.18 LTS, and Varnish Software 9.0.3.
This page is the HTTP/1 class. If you never passed -p feature=+http2, that CVE is not your listener. If you did, upgrade to 8.0.2 or 6.0.18, or turn the flag off with varnishadm param.set feature -http2 and remove h2 from the TLS terminator’s ALPN list. Do not keep HTTP/2 enabled on an 8.0.1 binary “because the CDN speaks it.” Terminate HTTP/2 on a patched proxy you own, or leave it off.
0.2. The proof does not need that date. varnishd -V on the host you run is the version that matters. If it is below the fixed line for the advisory you care about, schedule the restart.
Prove the object key on your own box
You are not sending a crafted body at someone else’s cache. You are proving your VCL returned pass for a cookie you already own, and hit only for a URL you marked public.
varnishd -Vis 8.0.2, 6.0.18, 7.7.1 or newer on that train, or you can name the exception.- Against your own origin,
curl -sIa public/css/app.csstwice. The secondAgeor varnish hit header should move. That URL has noCookie. - Replay a request you already make while logged in, with your own
Cookie, to/account. Expect a miss or a pass every time. A hit with your HTML in it is the bug. varnishlog -g request -q 'ReqURL eq "/account"'on your box should showVCL_return pass, nothit.
varnishd -V
# Expect a line at or after 8.0.2, 6.0.18, or 7.7.1.
curl -sI "https://your-app.example/css/app.css"
curl -sI "https://your-app.example/css/app.css"
# Cookie value comes from your own DevTools. Do not send it anywhere else.
curl -sI "https://your-app.example/account" \
-H "Cookie: session=PASTE_FROM_YOUR_DEVTOOLS"
# Expect no cached Age that grows across two calls while you stay logged in.
varnishlog -g request -q 'ReqURL eq "/account"'
# Expect: VCL_return pass
If the public CSS never hits, you left a site-wide cookie and never stripped it. That is a performance ticket, not a reason to override vcl_req_cookie. Strip the tracker. Keep the session. Keep pass on the session.
Questions we keep getting
Can I cache HTML for a logged-in user if I hash the session cookie?
No. That stores a personalized body. Anyone who can force a key collision, or who shares a CDN hop that ignores your extra hash, gets the wrong person. pass on Cookie is the control. Edge-side include of a public template plus a private API call is a different design. It is not “hash the cookie.”
Is HTTP/2 a substitute for the HTTP/1 work on this page?
No. HTTP/2 between the browser and Varnish still becomes HTTP/1 to many origins. VSV00019 is the reminder that the h2 parser has its own bugs when you turn +http2 on. Patch, or leave the flag off. The normalize-and-pass rules still apply on the HTTP/1 hop to 8080.
Does return (fail) on chunked bodies replace an upgrade?
No. It is the VSV00016 bridge from the advisory. It will drop honest chunked uploads. Upgrade to 7.7.1 or 6.0.14 or later, restart, then remove the fail if you added it only for that CVE.



