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]