This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new 21246cfbb2 Cleaner handling of response header message overflow
21246cfbb2 is described below

commit 21246cfbb2be2f609d9ae979cfa8b72826ae36c9
Author: remm <[email protected]>
AuthorDate: Thu Sep 3 16:02:23 2026 +0200

    Cleaner handling of response header message overflow
    
    Error and close the connection.
    Found by code review.
---
 java/org/apache/coyote/ajp/AjpMessage.java         | 12 +++++
 java/org/apache/coyote/ajp/AjpProcessor.java       | 44 +++++++++++++++--
 java/org/apache/coyote/ajp/LocalStrings.properties |  1 +
 .../coyote/ajp/TestAbstractAjpProcessor.java       | 56 ++++++++++++++++++++++
 webapps/docs/changelog.xml                         |  4 ++
 5 files changed, 113 insertions(+), 4 deletions(-)

diff --git a/java/org/apache/coyote/ajp/AjpMessage.java 
b/java/org/apache/coyote/ajp/AjpMessage.java
index ff92e1de6f..590b5d30fa 100644
--- a/java/org/apache/coyote/ajp/AjpMessage.java
+++ b/java/org/apache/coyote/ajp/AjpMessage.java
@@ -187,6 +187,18 @@ public class AjpMessage {
     }
 
 
+    /**
+     * Check if the given number of bytes can be appended to the message 
without overflowing the message buffer.
+     *
+     * @param numBytes The number of bytes to append
+     *
+     * @return {@code true} if the bytes can be appended without overflowing 
the buffer
+     */
+    public boolean hasRoom(int numBytes) {
+        return pos + numBytes <= buf.length;
+    }
+
+
     /**
      * Copy a chunk of bytes into the packet, starting at the current write 
position. The chunk of bytes is encoded with
      * the length in two bytes first, then the data itself, and finally a 
terminating \0 (which is <B>not</B> included
diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java 
b/java/org/apache/coyote/ajp/AjpProcessor.java
index 82d43adfc7..15bbdb2b1c 100644
--- a/java/org/apache/coyote/ajp/AjpProcessor.java
+++ b/java/org/apache/coyote/ajp/AjpProcessor.java
@@ -993,15 +993,45 @@ public class AjpProcessor extends AbstractProcessor {
 
             for (int i = 0; i < numHeaders; i++) {
                 try {
-                    // Write headers
                     MessageBytes hN = headers.getName(i);
                     int hC = Constants.getResponseAjpIndex(hN.toString());
+                    // Calculate the number of bytes the header name and value
+                    // will occupy in the AJP message
+                    int headerSize;
+                    if (hC > 0) {
+                        // The header name is encoded as a 2 byte integer
+                        headerSize = 2;
+                    } else {
+                        hN.toBytes();
+                        // 2 byte length, data, terminating \0
+                        headerSize = hN.getByteChunk().getLength() + 3;
+                    }
+                    MessageBytes hV = headers.getValue(i);
+                    if (hV == null) {
+                        // A null value is encoded as a 0 length string
+                        headerSize += 3;
+                    } else {
+                        hV.toBytes();
+                        // 2 byte length, data, terminating \0
+                        headerSize += hV.getByteChunk().getLength() + 3;
+                    }
+
+                    if (!responseMessage.hasRoom(headerSize)) {
+                        // AJP does not support splitting a header across
+                        // multiple packets so fail the response.
+                        
log.error(sm.getString("ajpprocessor.response.headerTooLarge", hN.toString(),
+                                Integer.toString(hV == null ? 0 : 
hV.getByteChunk().getLength()),
+                                
Integer.toString(responseMessage.getBuffer().length)));
+                        setErrorState(ErrorState.CLOSE_NOW, null);
+                        return;
+                    }
+
+                    // Write headers
                     if (hC > 0) {
                         responseMessage.appendInt(hC);
                     } else {
                         responseMessage.appendBytes(hN);
                     }
-                    MessageBytes hV = headers.getValue(i);
                     responseMessage.appendBytes(hV);
                 } catch (IllegalArgumentException iae) {
                     // Log the problematic header
@@ -1028,7 +1058,7 @@ public class AjpProcessor extends AbstractProcessor {
     protected final void flush() throws IOException {
         // Calling code should ensure that there is no data in the buffers for
         // non-blocking writes.
-        if (!responseFinished) {
+        if (!responseFinished && getErrorState().isIoAllowed()) {
             if (protocol.getAjpFlush()) {
                 // Send the flush message
                 socketWrapper.write(true, flushMessageArray, 0, 
flushMessageArray.length);
@@ -1046,6 +1076,12 @@ public class AjpProcessor extends AbstractProcessor {
 
         responseFinished = true;
 
+        if (!getErrorState().isIoAllowed()) {
+            // The response was failed before it was sent so there is nothing
+            // to finish and the connection will be closed.
+            return;
+        }
+
         // Swallow the unread body packet if present
         if (waitingForBodyMessage || first && request.getContentLengthLong() > 
0) {
             refillReadBuffer(true);
@@ -1342,7 +1378,7 @@ public class AjpProcessor extends AbstractProcessor {
             }
 
             int len = 0;
-            if (!swallowResponse) {
+            if (!swallowResponse && getErrorState().isIoAllowed()) {
                 try {
                     len = chunk.remaining();
                     writeData(chunk);
diff --git a/java/org/apache/coyote/ajp/LocalStrings.properties 
b/java/org/apache/coyote/ajp/LocalStrings.properties
index daf97d0f53..bbdec1c50d 100644
--- a/java/org/apache/coyote/ajp/LocalStrings.properties
+++ b/java/org/apache/coyote/ajp/LocalStrings.properties
@@ -34,6 +34,7 @@ ajpprocessor.request.invalidHeader=Header code [{0}] was 
invalid
 ajpprocessor.request.invalidMethod=Method code [{0}] was invalid
 ajpprocessor.request.prepare=Error preparing request
 ajpprocessor.request.process=Error processing request
+ajpprocessor.response.headerTooLarge=Response header [{0}] with value length 
[{1}] does not fit in an AJP packet of [{2}] bytes. Failing the response and 
closing the connection.
 ajpprocessor.response.invalidHeader=The HTTP response header [{0}] with value 
[{1}] has been removed from the response because it is invalid
 ajpprocessor.unexpectedMessage=Unexpected message type [{0}]
 ajpprocessor.unknownAttribute=Rejecting request due to unknown request 
attribute [{0}] received from reverse proxy
diff --git a/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java 
b/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java
index 76d49975d0..284ad42d58 100644
--- a/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java
+++ b/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java
@@ -941,6 +941,62 @@ public class TestAbstractAjpProcessor extends 
TomcatBaseTest {
     }
 
 
+    /*
+     * AJP does not support splitting a response header across multiple
+     * packets so a response header that does not fit in a single packet
+     * must fail the response rather than corrupt the AJP message.
+     */
+    @Test
+    public void testResponseHeaderLargerThanPacket() throws Exception {
+
+        Tomcat tomcat = getTomcatInstance();
+
+        // No file system docBase required
+        Context ctx = getProgrammaticRootContext();
+
+        Tomcat.addServlet(ctx, "largeHeader", new LargeHeaderServlet());
+        ctx.addServletMapping("/", "largeHeader");
+
+        tomcat.start();
+
+        SimpleAjpClient ajpClient = new SimpleAjpClient();
+        ajpClient.setPort(getPort());
+        ajpClient.connect();
+
+        validateCpong(ajpClient.cping());
+
+        TesterAjpMessage forwardMessage = ajpClient.createForwardMessage();
+        forwardMessage.end();
+
+        // The response header is larger than an AJP packet so the response
+        // is failed and the connection is closed without a response.
+        try {
+            ajpClient.sendMessage(forwardMessage);
+            Assert.fail("Expected the connection to be closed");
+        } catch (IOException ioe) {
+            // Expected
+        }
+
+        ajpClient.disconnect();
+    }
+
+
+    private static class LargeHeaderServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+            StringBuilder value = new StringBuilder(Constants.MAX_PACKET_SIZE 
* 2);
+            for (int i = 0; i < value.capacity(); i++) {
+                value.append('A');
+            }
+            resp.setHeader("X-Large-Header", value.toString());
+            resp.getWriter().print("Body");
+        }
+    }
+
+
     /**
      * Process response header packet and checks the status. Any other data is 
ignored.
      */
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 5f6e4a4fa8..548eb64f65 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -243,6 +243,10 @@
       <fix>
         Stricter OCSP handling when soft-fail is disabled. (markt)
       </fix>
+      <fix>
+        Cleaner handling of AJP response headers which overflow the maximum
+        message size. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Jasper">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to