# Variant RCA Report: CVE-2026-50052 / VSV00019

## Summary

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 in `bin/vinyld/http2/cache_http2_hpack.c` (lines 171–216) by converting each `!Tstrcmp(...)` check to `Tstreq(...)` and adding a new `Tstreq` macro in `include/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 `vtest2` submodule still contains a `Tstrcmp` macro, 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.

## Impact

- 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`); fixed `9.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 `:a` pseudo-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 `:a` vector 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:** `none` for the tested variants as a bypass; `partial` for the alternate trigger on the vulnerable version (same `:a` vector, 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`:

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

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

1. Run the variant reproduction script:
   ```bash
   bash bundle/vuln_variant/reproduction_steps.sh
   ```
2. 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.
3. The script creates per-variant VTC files under `bundle/vuln_variant/` and writes execution logs under `bundle/logs/`.
4. Expected evidence:
   - On the vulnerable commit, the baseline `:a` variant logs `EXPECT req.url (/smuggled) == "/smuggled" match` and the VTC passes.
   - On the fixed commit, every prefix variant logs `rst->err: PROTOCOL_ERROR` and does not match the smuggled request.
   - The upstream `f00019.vtc` regression test passes on the fixed commit.

## Evidence

- Variant summary: `bundle/logs/variant_summary.txt`
- Baseline `:a` vulnerable log: `bundle/logs/variant_ca_vuln.log`
- Baseline `:a` fixed 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 if` pseudo-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.2` tag; the fixed code was tested through the existing `repo-fixed` worktree at `9.0.3`.
- The only zero-length-header smuggling vector observed was the original `:a` prefix of `:authority`. All other prefixes either matched a different pseudo-header (producing a non-empty malformed header) or were rejected as duplicates/unknown.
