# MAD Bugs: My Cousin Vinyl (CVE-2026-50052)

Prefix-matching Tstrcmp in Vinyl/Varnish HPACK pseudo-header handling let a two-byte :a header desync HTTP/2-to-HTTP/1.1 translation, enabling request smuggling and credential theft.

- Payout: Research
- Severity: critical
- Program: Vinyl Cache
- Write-up by: Jun Rong
- Disclosed: 2026-07-01
- CVE: CVE-2026-50052
- Write-up quality: 9/10
- Original: https://blog.calif.io/p/mad-bugs-my-cousin-vinyl-cve-2026
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-calif-io-p-mad-bugs-my-cousin-vinyl-cve-2026
- Tags: area:cloud domain:infra domain:web impact:ato impact:data-leak kind:research platform:research sev:critical technique:chained technique:source-review vuln:cache-poisoning vuln:crlf-injection vuln:request-smuggling

**Target:** HTTP/2 caching proxy HPACK decoder / HTTP/1.1 backend translation
**Root cause:** Tstrcmp compares using the decoded header name's length rather than the target string's length, so any prefix of :authority (e.g. :a) matches the pseudo-header branch. The in-place rewrite then advances hdr.b past hdr.e, producing a zero-length header that emits a bare CRLF into the backend request.
**Steps:**
1. Send HTTP/2 request with pseudo-header `:a` (prefix of :authority) with a two-byte value so hdr.b += 6 lands exactly on hdr.e, avoiding the assert crash and storing a zero-length header.
2. Place a `content-length` header before the truncation point (pseudo-header ordering is not enforced per RFC 9113 8.3).
3. Vinyl translates to HTTP/1.1; the zero-length header emits only its `\r\n` suffix, terminating the backend header block early.
4. Backend reads exactly 109 bytes of body, consuming Vinyl's leftover headers; attacker's HTTP/2 DATA frame (exactly 109 bytes) is parsed as a new HTTP/1.1 request on the pooled connection.
5. Smuggled request declares a content-length larger than its body so the backend keeps reading; when Vinyl reuses the connection for a victim, the victim's request (Authorization, Cookie) becomes the body.
6. Victim's credentials are delivered to an attacker-chosen endpoint that stores/echoes bodies (e.g. POST /api/review); attacker polls /reviews to read them back.
**How found:** AI subagents audited HPACK decoder, HTTP/1.1 parser, and HTTP/2 frame layer. First subagent saw the memcpy and assumed Tstrcmp was a real equality check. Second subagent hit an assert(b <= e) abort and realized Tstrcmp only compares Tlen(t) bytes, so any prefix matches. A third model then reasoned that a two-byte value avoids the crash and yields a zero-length header, leading to the smuggling chain.
**Exploitation:** Attacker probes the post-:a leftover length for the deployment, then sends crafted HTTP/2 requests to poison a pooled backend connection. A smuggled POST with oversized content-length captures the next victim's full request including Authorization and Cookie, delivered to a body-storing endpoint the attacker polls. Demonstrated with Docker Compose Flask webstore behind Vinyl 7.6 + hitch; captured Basic auth `alice:i_love_c_programming` within 7.3s.
**Escalation:** Chained prefix-match bug to zero-length header to CRLF injection to request smuggling to cache poisoning/credential theft; content-length trick used twice (once to survive truncation, once to swallow the victim request).
**Insight:** When auditing protocol translators, inspect the comparison macros themselves (length semantics of strncmp wrappers) rather than trusting call sites; a prefix-match on a pseudo-header name plus in-place pointer arithmetic can turn a crash into a desync. Also check whether pseudo-header ordering is enforced, since a content-length before the truncation point converts a 400 into a smuggle.

