From 2166d651295608ac441fef21b9f7bc4f71092b9a Mon Sep 17 00:00:00 2001 From: Mo Chen Date: Thu, 25 Jun 2026 10:47:51 -0500 Subject: [PATCH] authproxy: strip request-body framing from auth sub-requests The head/range/redirect auth transforms copy the whole client request and then force it bodyless (method override + Content-Length: 0) to probe the auth server, but left Transfer-Encoding, Trailer, and Expect in place. A chunked or Expect: 100-continue client request therefore produced a self-contradictory sub-request: a bodyless HEAD/GET still advertising a body. ATS honors the framing and sets up a request-body tunnel for a body that never arrives, stalling the probe until the inactivity timeout; and proxy.config.http.reject_head_with_content rejects a HEAD that declares content outright. Strip Transfer-Encoding, Trailer, and Expect when normalizing the sub-request to bodyless. --- plugins/authproxy/authproxy.cc | 25 +++++++++++++++++++++++++ plugins/authproxy/utils.cc | 12 ++++++++++++ plugins/authproxy/utils.h | 3 +++ 3 files changed, 40 insertions(+) diff --git a/plugins/authproxy/authproxy.cc b/plugins/authproxy/authproxy.cc index 9edb1af3f3f..3d697241a48 100644 --- a/plugins/authproxy/authproxy.cc +++ b/plugins/authproxy/authproxy.cc @@ -296,6 +296,16 @@ AuthWriteHeadRequest(AuthRequestContext *auth) // Next, we need to rewrite the client request URL to be a HEAD request. TSReleaseAssert(TSHttpHdrMethodSet(rq.buffer, rq.header, TS_HTTP_METHOD_HEAD, -1) == TS_SUCCESS); + // This sub-request is bodyless (HEAD + Content-Length: 0), but the copied + // client request may carry request-body framing (e.g. a chunked POST or + // Expect: 100-continue). Left in place it is self-contradictory: ATS sets up + // a request-body tunnel for a body that never arrives (stalling the probe + // until timeout), and proxy.config.http.reject_head_with_content rejects a + // HEAD that declares content. Strip the framing when forcing Content-Length: 0. + HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_TRANSFER_ENCODING); + HttpRemoveMimeHeader(rq.buffer, rq.header, "Trailer"); + HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_EXPECT); + HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CONTENT_LENGTH, 0u); HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CACHE_CONTROL, "no-cache"); @@ -333,6 +343,13 @@ AuthWriteRangeRequest(AuthRequestContext *auth) TSReleaseAssert(TSHttpHdrMethodSet(rq.buffer, rq.header, TS_HTTP_METHOD_GET, -1) == TS_SUCCESS); } + // The body is dropped (Content-Length: 0), so strip any request-body framing + // (Transfer-Encoding/Trailer/Expect) copied from the client request to keep + // the sub-request well-formed. + HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_TRANSFER_ENCODING); + HttpRemoveMimeHeader(rq.buffer, rq.header, "Trailer"); + HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_EXPECT); + HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CONTENT_LENGTH, 0u); HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_RANGE, "bytes=0-0"); HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CACHE_CONTROL, "no-cache"); @@ -386,6 +403,14 @@ AuthWriteRedirectedRequest(AuthRequestContext *auth) TSHandleMLocRelease(rq.buffer, rq.header, murl); HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_HOST, hostbuf); + + // The body is dropped (Content-Length: 0), so strip any request-body framing + // (Transfer-Encoding/Trailer/Expect) copied from the client request to keep + // the sub-request well-formed. + HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_TRANSFER_ENCODING); + HttpRemoveMimeHeader(rq.buffer, rq.header, "Trailer"); + HttpRemoveMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_EXPECT); + HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CONTENT_LENGTH, 0u); HttpSetMimeHeader(rq.buffer, rq.header, TS_MIME_FIELD_CACHE_CONTROL, "no-cache"); diff --git a/plugins/authproxy/utils.cc b/plugins/authproxy/utils.cc index 15732089070..ce55d40c26a 100644 --- a/plugins/authproxy/utils.cc +++ b/plugins/authproxy/utils.cc @@ -101,6 +101,18 @@ HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string_view name, cons TSHandleMLocRelease(mbuf, mhdr, mloc); } +void +HttpRemoveMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name) +{ + TSMLoc mloc; + + // A field may be repeated on multiple lines; destroy every instance. + while ((mloc = TSMimeHdrFieldFind(mbuf, mhdr, name, -1)) != TS_NULL_MLOC) { + TSReleaseAssert(TSMimeHdrFieldDestroy(mbuf, mhdr, mloc) == TS_SUCCESS); + TSHandleMLocRelease(mbuf, mhdr, mloc); + } +} + unsigned HttpGetContentLength(TSMBuffer mbuf, TSMLoc mhdr) { diff --git a/plugins/authproxy/utils.h b/plugins/authproxy/utils.h index a36a0a90bd9..5291baa88c8 100644 --- a/plugins/authproxy/utils.h +++ b/plugins/authproxy/utils.h @@ -108,6 +108,9 @@ void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name, const char void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name, unsigned value); void HttpSetMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string_view name, const std::string_view value); +// Remove every instance of an HTTP header. +void HttpRemoveMimeHeader(TSMBuffer mbuf, TSMLoc mhdr, const char *name); + // Dump the given HTTP header to the debug log. void HttpDebugHeader(TSMBuffer mbuf, TSMLoc mhdr);