# Patch Analysis: CVE-2026-50052 / VSV00019 (Varnish/Vinyl Cache HTTP/2 request smuggling)

## Fix commit / tested version

- Fixed tag: `varnish-9.0.3`
- Fixed commit tested: `0a625649cd40af4b6c10be5e58a2e89a5e275baa`
- Vulnerable commit tested: `dc27a2dce662015cf79bbda5ff193b6bb74ba2d1` (`varnish-9.0.2`)
- Repository: `https://github.com/varnish/varnish.git`

## Files changed by the fix

```
 bin/vinyld/http2/cache_http2_hpack.c | 10 +++++-----
 bin/vinyltest/tests/f00019.vtc       | 31 +++++++++++++++++++++++++++++++
 configure.ac                         |  2 +-
 doc/changes.rst                      |  9 +++++++++
 include/vdef.h                       |  2 +-
 5 files changed, 47 insertions(+), 7 deletions(-)
```

The security-relevant changes are in two files:

1. **`include/vdef.h`** – introduces a new length-safe string-equality macro `Tstreq`:
   ```c
   #define Tstreq(t, s) (Tlen(t) == strlen(s) && !vmemcmp((t).b, (s), Tlen(t)))
   ```
   The old macro `Tstrcmp` was:
   ```c
   #define Tstrcmp(t, s) (strncmp((t).b, (s), Tlen(t)))
   ```
   `Tstrcmp` only compares the first `Tlen(t)` bytes of the constant string `s`, which means **any HPACK pseudo-header name that is a prefix of a known pseudo-header matched** (e.g. `:a` matches `:authority`).

2. **`bin/vinyld/http2/cache_http2_hpack.c`** – in `h2h_addhdr()`, the four pseudo-header match checks are changed from `!Tstrcmp(...)` to `Tstreq(...)`:
   - `:method`
   - `:path`
   - `:scheme`
   - `:authority`
   The `:path` "must be `*`" check is also changed from `Tstrcmp(val, "*")` to `!Tstreq(val, "*")` for the same reason.

3. **`bin/vinyltest/tests/f00019.vtc`** – new regression test that explicitly sends `:a`, `:p`, `:s`, `:m` and expects `RST_STREAM` with `PROTOCOL_ERROR` on the fixed version.

## What the fix assumes

- The complete set of valid HTTP/2 request pseudo-headers is exactly the four named in `h2h_addhdr()`: `:method`, `:path`, `:scheme`, `:authority`.
- Any HPACK field name starting with `:` that is **not** one of those four exact strings is malicious/unknown and must be rejected with `H2SE_PROTOCOL_ERROR`.
- Exact equality (both length and content) is sufficient to prevent the prefix confusion that allowed `:a` to be treated as `:authority`.

## What the fix does NOT cover

- It does **not** change the serialization path to the backend HTTP/1 connection; the bare `\r\n` injection path remains reachable in principle, but only if the parser can still be tricked into producing a zero-length header.
- It does **not** address other classes of HTTP request smuggling that do not rely on H2 pseudo-header prefix matching (e.g., `Transfer-Encoding` / `Content-Length` interleaving, HTTP/1.1 parsing differences, or HTTP/2 CONTINUATION frame issues not involving the pseudo-header name).
- It does **not** remove the `Tstrcmp` macro from the `vtest2` submodule (used only by the test harness), but that copy is not on the request-handling path.
- It does **not** add a generic lookup table or case-folding; it keeps the four explicit `if/else if` checks. If a future standard adds a new pseudo-header, this function will need to be updated to recognize it, otherwise the new pseudo-header will be rejected as unknown.

## Behavior before and after the fix

| Input | Vulnerable `9.0.2` | Fixed `9.0.3` |
|---|---|---|
| `:a` pseudo-header (prefix of `:authority`) | Matched as `:authority`, produced a zero-length header, injected `\r\n`, backend parsed the DATA body as a second request (`GET /smuggled`) | Rejected as unknown pseudo-header → `RST_STREAM PROTOCOL_ERROR` |
| `:m` (prefix of `:method`) | Matched as `:method`; value `xx` became the method token; no smuggling observed | Rejected → `PROTOCOL_ERROR` |
| `:p` / `:s` (prefixes of `:path` / `:scheme`) | Matched as the corresponding pseudo-header; no zero-length header, no smuggling observed | Rejected → `PROTOCOL_ERROR` |
| `:au` / `:auth` (longer prefixes of `:authority`) | Matched as `:authority`, produced a malformed non-empty header (`h` / `ho` …); did not yield a second request on the backend | Rejected → `PROTOCOL_ERROR` |
| `:authorityfoo` (not a prefix of a known pseudo-header) | Rejected as unknown pseudo-header | Rejected as unknown pseudo-header |
| Upstream `f00019.vtc` | N/A | Passes, confirming all tested prefixes are rejected |

## Threat model / security-policy context

The target's security advisory (VSV00019) states:

- The attack vector only exists when HTTP/2 is enabled (`feature=+http2`). HTTP/2 is disabled by default.
- The recommended mitigations are upgrading to a fixed version, disabling HTTP/2, or applying a VCL-level desync-closing filter.
- The advisory classifies the issue as HTTP/2 request smuggling with potential cache poisoning / authentication bypass.

The fix is therefore in-scope and is the code-level fix the advisory recommends. The variant search stays within that same trust boundary: an untrusted HTTP/2 client sending crafted H2 frames to `varnishd`.

## Verdict on completeness

For the reported root cause (H2 pseudo-header prefix confusion in `h2h_addhdr`), the fix is **complete**. The change from `Tstrcmp` to `Tstreq` removes the prefix-match semantics entirely, and the upstream regression test covers all obvious prefix variants (`:a`, `:m`, `:p`, `:s`).

No runtime bypass was found against the fixed `9.0.3` commit for any tested prefix pseudo-header. The original `:a` vector is still exploitable on the vulnerable `9.0.2` commit but is blocked on the fixed version.
