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);
 

Reply via email to