CVE-2026-50052: Verified Repro With Script Download
CVE-2026-50052: HTTP/2 request smuggling in Vinyl Cache and Varnish Cache VSV00019
CVE-2026-50052 is verified against varnish/varnish · github. Vulnerability class: Request Smuggling. This low reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00231.
What Is CVE-2026-50052?
CVE-2026-50052 (VSV00019) is an HTTP/2 request-smuggling vulnerability (CWE-444) in Varnish Cache and Vinyl Cache caused by a deficiency in HTTP/2 request parsing. Pruva reproduced it (reproduction REPRO-2026-00231).
CVE-2026-50052 Severity & CVSS Score
CVE-2026-50052 is rated low severity, with a CVSS base score of 2.3 out of 10.
Low — limited impact or hard to exploit. Address in the normal cycle.
How to Reproduce CVE-2026-50052
pruva-verify REPRO-2026-00231 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00231/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-50052
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
malformed HTTP/2 request with :a pseudo-header and content-length
- HTTP/2 request parsing in bin/vinyld/http2/cache_http2_hpack.c, serialized to backend HTTP/1 connection
No bypass found: the VSV00019 fix (Tstrcmp -> Tstreq) blocks all tested H2 pseudo-header prefix variants on varnish-9.0.3; the original :a vector still works on the vulnerable 9.0.2 commit.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-50052
CVE-2026-50052 (VSV00019) is an HTTP/2 request-smuggling vulnerability in Varnish/Vinyl Cache. When HTTP/2 support is enabled (feature=+http2), an attacker can send a malformed HTTP/2 request whose pseudo-header name :a is accepted as a prefix of :authority. The HPACK-to-HTTP/1 serialization in bin/vinyld/http2/cache_http2_hpack.c rewrites the header in-place, turning it into a zero-length header on the backend wire. This injects a bare \r\n into the HTTP/1 request header block, prematurely terminating the headers. By placing a content-length before the truncated header and then sending a DATA frame containing a valid HTTP/1 request, the attacker desynchronizes the frontend and backend, causing the backend to parse the DATA frame as a second request on a reused connection.
- Product / component: Varnish Cache / Vinyl Cache HTTP/2 frontend (
bin/vinyld/http2/cache_http2_hpack.c) and HTTP/1 backend serialization. - Affected versions: Varnish/Vinyl Cache 9.0.2 (and earlier affected lines 7.6.0–8.0.1, 6.0 LTS 6.0.14–6.0.17, Vinyl 9.0.0). This run reproduced the issue on
varnish-9.0.2(dc27a2dce662015cf79bbda5ff193b6bb74ba2d1). - Risk level / consequences: High. The attacker can desync a reused backend connection, leading to HTTP request smuggling, cache poisoning, and potential authentication bypass or information disclosure, as described in VSV00019.
Impact Parity
- Disclosed/claimed maximum impact: HTTP request smuggling (backend request desync) via malformed HTTP/2 requests, with follow-on effects such as cache poisoning and authentication bypass.
- Reproduced impact from this run: We demonstrated a real backend request desync: the vulnerable Varnish forwarded the attacker's crafted HTTP/2 request to a backend HTTP/1 server, and the backend received a second, attacker-controlled HTTP/1 request (
GET /smuggled) on the same pooled connection. The same payload against the fixed version (varnish-9.0.3) was rejected with an HTTP/2RST_STREAM/PROTOCOL_ERROR. - Parity:
partial– the exact network-level desync was reproduced, but the current harness stops short of demonstrating the downstream cache-poisoning / auth-bypass consequences. Those are the natural result of the desync primitive and are claimed by the advisory. - Not demonstrated: Full cache poisoning or credential theft. The PoC only proves the smuggling primitive that enables those attacks.
Root Cause
The bug is in the pseudo-header matching logic in bin/vinyld/http2/cache_http2_hpack.c, specifically in h2h_addhdr().
The vulnerable code used a macro Tstrcmp() defined in include/vdef.h as:
#define Tstrcmp(t, s) (strncmp((t).b, (s), Tlen(t)))
This compares only the first Tlen(t) bytes of the target string. As a result, any header name that is a prefix of :authority (e.g., :a, :au, :autho) is treated as :authority and enters the :authority branch. That branch writes "host" over the last four bytes of the header name and advances the start pointer by 6, leaving a zero-length header in the internal header array.
When the request is serialized to HTTP/1 for the backend, HTTP1_Write() emits a bare \r\n for that zero-length header, ending the header block early. The remaining headers (and any DATA frame) become the request body, letting a second HTTP/1 request follow on the same persistent backend connection.
The official fix is the Tstreq() macro that checks both length and content, and the migration of h2h_addhdr() to use exact equality for all four pseudo-header names. The fixed version was reproduced from varnish-9.0.3 (0a625649cd40af4b6c10be5e58a2e89a5e275baa).
Relevant upstream references:
- VSV00019 advisory: https://www.varnish.org/docs/security/vsv00019/
- Blog post with detailed exploit mechanics: https://blog.calif.io/p/mad-bugs-my-cousin-vinyl-cve-2026
- Fixed release tag:
varnish-9.0.3 - Vulnerable reproduction tag:
varnish-9.0.2
Reproduction Steps
All reproduction is driven by bundle/repro/reproduction_steps.sh. The script performs the following steps:
- Reads
bundle/project_cache_context.jsonto find the durable project cache, or falls back tobundle/artifacts/varnish. - Clones
https://github.com/varnish/varnish.gitinto the cache if needed and checks out the vulnerable tagvarnish-9.0.2. - Creates a Git worktree at
repo-fixedwith the fixed tagvarnish-9.0.3. - Installs build dependencies (
autoconf,automake,libpcre2-dev,libev-dev,libedit-dev,python3-docutils,python3-sphinx, etc.). - Builds both the vulnerable and the fixed source trees with autotools.
- Writes the custom
bin/vinyltest/tests/x_vsv00019.vtctest to both trees. The test:- Starts an HTTP/1 backend server that expects two requests.
- Starts
varnishdwithfeature=+http2and a VCL that stripsX-Varnish,Via,X-Forwarded-For, and setsHostempty so the byte layout is predictable. - Sends an HTTP/2 HEADERS frame with
-nostrendcontaining the pseudo-headers:method,:path,:scheme, a regularcontent-length: 35, the malicious pseudo-header:a: xx, and a padding headerx-pad: aaaaaaaaaaaaaaaa. - Sends a DATA frame whose 35-byte payload is the smuggled HTTP/1 request
GET /smuggled HTTP/1.1\r\nHost: x\r\n\r\n.
- Runs the test against the vulnerable build and captures the output to
bundle/logs/vulnsmuggle.log. - Runs the same test against the fixed build and captures the output to
bundle/logs/fixedsmuggle.log. - Verifies that the vulnerable run passes and that the backend receives
GET /smuggled, while the fixed run returnsRST_STREAMwithPROTOCOL_ERROR. - Writes
bundle/repro/runtime_manifest.jsonand a summary tobundle/artifacts/summary.txt.
Expected evidence of reproduction:
- In
vulnsmuggle.log: lines showing the backend received the first request (POST /attack), then a second request whose URL is/smuggled, and the test exits withx_vsv00019.vtc passed. - In
fixedsmuggle.log: the server never sees a second request; instead, Varnish responds withRST_STREAMandrst->err: PROTOCOL_ERROR.
Evidence
bundle/logs/vulnsmuggle.log– fullvarnishtest -voutput for the vulnerable run.bundle/logs/fixedsmuggle.log– fullvarnishtest -voutput for the fixed run.bundle/artifacts/summary.txt– parsed verdict summary (commit SHAs, boolean flags).bundle/repro/runtime_manifest.json– structured runtime evidence manifest.
Key excerpts from the vulnerable run (bundle/logs/vulnsmuggle.log):
**** s1 rxhdr|POST /attack HTTP/1.1\r
**** s1 rxhdr|scheme: http\r
**** s1 rxhdr|content-length: 35\r
**** s1 rxhdr|\r
**** s1 rxhdrlen = 59
...
**** s1 c-l|x-pad: aaaaaaaaaaaaaaaa\r
**** s1 c-l|Host: \r
**** s1 bodylen = 35
...
**** s1 rxhdr|GET /smuggled HTTP/1.1\r
...
**** s1 EXPECT req.url (/smuggled) == "/smuggled" match
...
# top TEST ... x_vsv00019.vtc passed (1.913)
Key excerpt from the fixed run (bundle/logs/fixedsmuggle.log):
*** c1 rx: stream: 1, type: RST_STREAM (3), flags: 0x00, size: 4
**** c1.1 rst->err: PROTOCOL_ERROR (1)
---- c1.1 Frame #1 for rxresp was of type RST_STREAM (3) instead of HEADERS (1)
...
# top TEST ... x_vsv00019.vtc FAILED (0.601) signal=6
Environment details captured:
- Vulnerable source SHA:
dc27a2dce662015cf79bbda5ff193b6bb74ba2d1(tagvarnish-9.0.2) - Fixed source SHA:
0a625649cd40af4b6c10be5e58a2e89a5e275baa(tagvarnish-9.0.3) - Build configured with
--disable-dependency-tracking, built withgccand GNU make.
Recommendations / Next Steps
- Fix approach: Apply the upstream fix that replaces
Tstrcmp()withTstreq()and enforces exact pseudo-header name matching inh2h_addhdr(). No other code should rely on prefix-only matching. - Upgrade guidance: Upgrade to a non-affected version: Varnish/Vinyl Cache 9.0.3, 8.0.2, or 6.0 LTS 6.0.18, or disable HTTP/2 by removing
+http2from thefeatureparameter until patched. - Testing recommendations: Add a regression test that sends an HTTP/2 request with the
:apseudo-header and verifies either aRST_STREAM/PROTOCOL_ERRORresponse or the absence of a smuggled request on the backend. The upstreamf00019.vtctest already covers this for the fixed branches.
Additional Notes
- Idempotency:
reproduction_steps.shis safe to run multiple times. It checks for existing clones and built binaries and skips expensive build steps when they are already present. Two consecutive runs in this session both confirmed the vulnerability. - Edge cases / limitations: The PoC uses a short
:avalue (xx) and a carefully sized padding header so that the zero-length header injects the exact\r\nneeded to terminate the backend header block. If Varnish added extra backend headers that could not be controlled, the byte offsets would need to be recalculated; the VCL used in the test removes those variables to make the reproduction deterministic. The PoC demonstrates the desync primitive but does not exercise the full cache-poisoning chain described in the advisory. - Entrypoint kind: This reproduction uses the real network protocol path:
vinyltest/varnishtestdrives a TCP connection to the realvarnishd, performs the HTTP/2 SETTINGS handshake, sends the crafted HEADERS/DATA frames, and observes the backend HTTP/1 conversation. The runtime manifest recordsentrypoint_kind: tcp_peer.
Variant Analysis & Alternative Triggers for CVE-2026-50052
No distinct bypass of the official VSV00019 fix was found. The official patch (0a625649cd40af4b6c10be5e58a2e89a5e275baa, varnish-9.0.3) replaces the prefix-matching Tstrcmp macro with the exact-match Tstreq macro in bin/vinyld/http2/cache_http2_hpack.c. This blocks every tested prefix pseudo-header variant (:a, :m, :p, :s, :au, :auth, :) on the fixed version. The original :a attack still reproduces on the vulnerable varnish-9.0.2 (dc27a2dce662015cf79bbda5ff193b6bb74ba2d1), but the fixed version returns RST_STREAM PROTOCOL_ERROR. The upstream regression test f00019.vtc explicitly covers :a, :m, :p, :s and passes on the fixed version, confirming the fix's intent. The only meaningful variant is the same :a pseudo-header prefix; it is not a bypass because it is also blocked by the patched code.
Fix Coverage / Assumptions
- The fix assumes the only valid H2 request pseudo-headers are
:method,:path,:scheme, and:authority, and that they must match exactly in both length and content. - It explicitly covers the
h2h_addhdr()decision block inbin/vinyld/http2/cache_http2_hpack.c(lines 171–216) by converting each!Tstrcmp(...)check toTstreq(...)and adding a newTstreqmacro ininclude/vdef.h. - It does not cover unrelated HTTP/1.1 or HTTP/2 desync vectors, and it does not add a generic lookup table for future pseudo-headers. The
vtest2submodule still contains aTstrcmpmacro, but that is test-harness code, not on the request-handling path.
Variant / Alternate Trigger
The variant search focused on the most natural class of alternate triggers: other prefix pseudo-header names that the old Tstrcmp macro would have matched as one of the four standard pseudo-headers. The tested candidates are:
| Variant | Reason tested | Result on vulnerable 9.0.2 | Result on fixed 9.0.3 |
|---|---|---|---|
:a |
Baseline (prefix of :authority) |
Backend desync reproduced (GET /smuggled received) |
Blocked: RST_STREAM PROTOCOL_ERROR |
: |
Shortest possible prefix of all four pseudo-headers | Rejected as duplicate pseudo-header (: matched :method) |
Blocked: PROTOCOL_ERROR |
:au |
Longer prefix of :authority |
Test passed but did not smuggle a second request; malformed h: header emitted |
Blocked: PROTOCOL_ERROR |
:auth |
Longer prefix of :authority |
Same as :au |
Blocked: PROTOCOL_ERROR |
:m |
Prefix of :method |
Treated as method; no second request observed | Blocked: PROTOCOL_ERROR |
:p |
Prefix of :path |
Treated as path; no second request observed | Blocked: PROTOCOL_ERROR |
:s |
Prefix of :scheme |
Treated as scheme; no second request observed | Blocked: PROTOCOL_ERROR |
:authorityfoo |
Negative control (not a prefix) | Rejected as unknown pseudo-header | Rejected as unknown pseudo-header |
Entry point: untrusted HTTP/2 client (tcp_peer) sends a crafted HEADERS frame to varnishd with the +http2 feature enabled. The vulnerable code path is bin/vinyld/http2/cache_http2_hpack.c:h2h_addhdr() → Tstrcmp prefix match → :authority branch → memcpy(d->out + 6, "host", 4) and hdr.b += 6, producing a zero-length header that serializes as a bare \r\n to the backend HTTP/1 connection.
- Product / component: Varnish Cache / Vinyl Cache HTTP/2 frontend (
bin/vinyld/http2/cache_http2_hpack.c) and HTTP/1 backend serialization. - Affected versions tested: vulnerable
9.0.2(dc27a2d); fixed9.0.3(0a62564). The advisory also lists affected 7.6.0–8.0.1 and 6.0 LTS 6.0.14–6.0.17, with corresponding fixes 8.0.2 and 6.0.18. - Risk level: The original vulnerability is high when HTTP/2 is enabled. The variant run confirms that the same root cause can be exercised with the same
:apseudo-header on the vulnerable version, but not on the fixed version.
Impact Parity
- Disclosed/claimed maximum impact: HTTP/2 request smuggling (backend request desync) leading to cache poisoning, authentication bypass, or information disclosure/manipulation.
- Reproduced impact from this variant run: Only the baseline
:avector reproduced the backend desync on the vulnerable commit. None of the other prefix variants produced a second attacker-controlled HTTP/1 request, and none of them bypassed the fixed version. - Parity:
nonefor the tested variants as a bypass;partialfor the alternate trigger on the vulnerable version (same:avector, no new path). - Not demonstrated: Cache poisoning, authentication bypass, or information disclosure chain. Only the primitive (backend request desync on a reused connection) was demonstrated, matching the repro stage.
Root Cause
The underlying bug is the Tstrcmp macro in include/vdef.h:
#define Tstrcmp(t, s) (strncmp((t).b, (s), Tlen(t)))
When t is the HPACK-decoded pseudo-header name, Tstrcmp returns 0 for any string that starts with t. Thus a header named :a (length 2) is considered equal to :authority (length 10) because strncmp(":a", ":authority", 2) == 0. The :authority handling code then does:
memcpy(d->out + 6, "host", 4);
hdr.b += 6;
For a 2-character name (:a), hdr.b ends up equal to the end of the serialized header, producing a zero-length header field. When the HTTP/1 backend serializer emits the header list, the zero-length field is emitted as a bare \r\n, prematurely terminating the HTTP/1 header block. The following DATA frame body (a complete HTTP/1 request) is then parsed by the backend as a second request on the same connection.
The fix changes the match to Tstreq, which requires both the same length and the same bytes, so :a no longer matches :authority and falls into the else if (nm.b[0] == ':') branch that returns H2SE_PROTOCOL_ERROR.
- Link to fix commit:
0a625649cd40af4b6c10be5e58a2e89a5e275baa(varnish-9.0.3) - Link to vulnerable commit:
dc27a2dce662015cf79bbda5ff193b6bb74ba2d1(varnish-9.0.2)
Reproduction Steps
- Run the variant reproduction script:
bash bundle/vuln_variant/reproduction_steps.sh - The script reuses the Varnish/Vinyl Cache source and binaries built by the repro stage. It tests the pseudo-header prefix variants listed above against both the vulnerable (
9.0.2) and fixed (9.0.3) commits. - The script creates per-variant VTC files under
bundle/vuln_variant/and writes execution logs underbundle/logs/. - Expected evidence:
- On the vulnerable commit, the baseline
:avariant logsEXPECT req.url (/smuggled) == "/smuggled" matchand the VTC passes. - On the fixed commit, every prefix variant logs
rst->err: PROTOCOL_ERRORand does not match the smuggled request. - The upstream
f00019.vtcregression test passes on the fixed commit.
- On the vulnerable commit, the baseline
Evidence
- Variant summary:
bundle/logs/variant_summary.txt - Baseline
:avulnerable log:bundle/logs/variant_ca_vuln.log - Baseline
:afixed log:bundle/logs/variant_ca_fixed.log - Upstream regression test log:
bundle/logs/variant_f00019_fixed.log - Per-variant logs:
bundle/logs/variant_<safe_name>_{vuln,fixed}.log
Key excerpts from the summary:
HEADER VULN_SM VULN_PASS FIX_SM FIX_RST
:a yes yes no yes
: no no no yes
:au no yes no yes
:auth no yes no yes
:m no no no yes
:p no no no yes
:s no no no yes
:authorityfoo no no no yes
f00019_fixed=passed
Recommendations / Next Steps
- The official fix is complete for the reported root cause. No additional code change is required for the prefix-match class of variants.
- Hardening suggestion: replace the four explicit
if/else ifpseudo-header checks with a bounded lookup table or a length-checked switch so that future pseudo-headers cannot be accidentally matched by prefix. - Operators who cannot upgrade should continue to follow the VSV00019 advisory: disable HTTP/2 or deploy the VCL-level desync-closing mitigation.
- Future variant work should monitor for other HTTP/2 desync primitives not addressed by this patch (e.g., HTTP/1.1 interleaving, CONTINUATION frame edge cases, or new H2 extensions).
Additional Notes
- The script is idempotent: it does not mutate the source checkout and can be rerun; it will overwrite the per-variant VTC files and logs with fresh results.
- The repo checkout state was preserved at the vulnerable
9.0.2tag; the fixed code was tested through the existingrepo-fixedworktree at9.0.3. - The only zero-length-header smuggling vector observed was the original
:aprefix of:authority. All other prefixes either matched a different pseudo-header (producing a non-empty malformed header) or were rejected as duplicates/unknown.
CVE-2026-50052 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-50052
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-50052
FAQ: CVE-2026-50052
How does the CVE-2026-50052 HTTP/2 request-smuggling attack work?
Which Varnish/Vinyl Cache versions are affected by CVE-2026-50052, and where is it fixed?
How severe is CVE-2026-50052?
How can I reproduce CVE-2026-50052?
References for CVE-2026-50052
Authoritative sources for CVE-2026-50052 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.