# RCA Report: CVE-2026-50052 / VSV00019 – HTTP/2 Request Smuggling in Varnish/Vinyl Cache

## Summary

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.

## Impact

- **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/2 `RST_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:

```c
#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:

1. Reads `bundle/project_cache_context.json` to find the durable project cache, or falls back to `bundle/artifacts/varnish`.
2. Clones `https://github.com/varnish/varnish.git` into the cache if needed and checks out the vulnerable tag `varnish-9.0.2`.
3. Creates a Git worktree at `repo-fixed` with the fixed tag `varnish-9.0.3`.
4. Installs build dependencies (`autoconf`, `automake`, `libpcre2-dev`, `libev-dev`, `libedit-dev`, `python3-docutils`, `python3-sphinx`, etc.).
5. Builds both the vulnerable and the fixed source trees with autotools.
6. Writes the custom `bin/vinyltest/tests/x_vsv00019.vtc` test to both trees. The test:
   - Starts an HTTP/1 backend server that expects two requests.
   - Starts `varnishd` with `feature=+http2` and a VCL that strips `X-Varnish`, `Via`, `X-Forwarded-For`, and sets `Host` empty so the byte layout is predictable.
   - Sends an HTTP/2 HEADERS frame with `-nostrend` containing the pseudo-headers `:method`, `:path`, `:scheme`, a regular `content-length: 35`, the malicious pseudo-header `:a: xx`, and a padding header `x-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`.
7. Runs the test against the vulnerable build and captures the output to `bundle/logs/vulnsmuggle.log`.
8. Runs the same test against the fixed build and captures the output to `bundle/logs/fixedsmuggle.log`.
9. Verifies that the vulnerable run passes and that the backend receives `GET /smuggled`, while the fixed run returns `RST_STREAM` with `PROTOCOL_ERROR`.
10. Writes `bundle/repro/runtime_manifest.json` and a summary to `bundle/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 with `x_vsv00019.vtc passed`.
- In `fixedsmuggle.log`: the server never sees a second request; instead, Varnish responds with `RST_STREAM` and `rst->err: PROTOCOL_ERROR`.

## Evidence

- `bundle/logs/vulnsmuggle.log` – full `varnishtest -v` output for the vulnerable run.
- `bundle/logs/fixedsmuggle.log` – full `varnishtest -v` output 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` (tag `varnish-9.0.2`)
- Fixed source SHA: `0a625649cd40af4b6c10be5e58a2e89a5e275baa` (tag `varnish-9.0.3`)
- Build configured with `--disable-dependency-tracking`, built with `gcc` and GNU make.

## Recommendations / Next Steps

- **Fix approach:** Apply the upstream fix that replaces `Tstrcmp()` with `Tstreq()` and enforces exact pseudo-header name matching in `h2h_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 `+http2` from the `feature` parameter until patched.
- **Testing recommendations:** Add a regression test that sends an HTTP/2 request with the `:a` pseudo-header and verifies either a `RST_STREAM`/`PROTOCOL_ERROR` response or the absence of a smuggled request on the backend. The upstream `f00019.vtc` test already covers this for the fixed branches.

## Additional Notes

- **Idempotency:** `reproduction_steps.sh` is 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 `:a` value (`xx`) and a carefully sized padding header so that the zero-length header injects the exact `\r\n` needed 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/varnishtest` drives a TCP connection to the real `varnishd`, performs the HTTP/2 SETTINGS handshake, sends the crafted HEADERS/DATA frames, and observes the backend HTTP/1 conversation. The runtime manifest records `entrypoint_kind: tcp_peer`.
