This is an automated email from the ASF dual-hosted git repository.
moonchen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 364668f365 authproxy: strip request-body framing from auth
sub-requests (#13333)
364668f365 is described below
commit 364668f365a6be7fe0b9619d345ecdc8a3b2ecab
Author: Mo Chen <[email protected]>
AuthorDate: Sat Jul 11 19:01:57 2026 -0500
authproxy: strip request-body framing from auth sub-requests (#13333)
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 9edb1af3f3..3d697241a4 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 1573208907..ce55d40c26 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 a36a0a90bd..5291baa88c 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);