This is an automated email from the ASF dual-hosted git repository. markt-asf pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 9a2b13306e59b61cdc62d8e3b0cdcb12d176e504 Author: Mark Thomas <[email protected]> AuthorDate: Wed Sep 30 07:44:15 2026 +0100 Add a test case for CVE-2026-103119 --- .../coyote/ajp/TestAbstractAjpProcessor.java | 6 +- test/org/apache/coyote/ajp/TestAjpSecurity.java | 159 +++++++++++++++++++++ 2 files changed, 162 insertions(+), 3 deletions(-) diff --git a/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java b/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java index 0f5b80d963..83a0600bd8 100644 --- a/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java +++ b/test/org/apache/coyote/ajp/TestAbstractAjpProcessor.java @@ -995,10 +995,10 @@ public class TestAbstractAjpProcessor extends TomcatBaseTest { } - /** + /* * Process response header packet and checks the status. Any other data is ignored. */ - private Map<String, List<String>> validateResponseHeaders(TesterAjpMessage message, int expectedStatus, + static Map<String, List<String>> validateResponseHeaders(TesterAjpMessage message, int expectedStatus, String expectedMessage) throws Exception { // First two bytes should always be AB Assert.assertEquals((byte) 'A', message.buf[0]); @@ -1088,7 +1088,7 @@ public class TestAbstractAjpProcessor extends TomcatBaseTest { Assert.assertEquals(Boolean.valueOf(expectedReuse), Boolean.valueOf(reuse)); } - private void validateCpong(TesterAjpMessage message) throws Exception { + static void validateCpong(TesterAjpMessage message) throws Exception { // First two bytes should always be AB Assert.assertEquals((byte) 'A', message.buf[0]); Assert.assertEquals((byte) 'B', message.buf[1]); diff --git a/test/org/apache/coyote/ajp/TestAjpSecurity.java b/test/org/apache/coyote/ajp/TestAjpSecurity.java new file mode 100644 index 0000000000..df4cea10bf --- /dev/null +++ b/test/org/apache/coyote/ajp/TestAjpSecurity.java @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.coyote.ajp; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.HexUtils; + +/* + * Tests that can't easily be added to the {@code org.apache.tomcat.security} package because they need access to + * package private classes. + */ +public class TestAjpSecurity extends TomcatBaseTest { + + + @Test + public void testCVE_2026_103119() throws Exception { + + Tomcat tomcat = getTomcatInstance(); + + // No file system docBase required + Context ctx = getProgrammaticRootContext(); + + Wrapper w = Tomcat.addServlet(ctx, "largePayload", new LargePayloadServlet()); + w.setAsyncSupported(true); + ctx.addServletMapping("/", "largePayload"); + + Connector c = tomcat.getConnector(); + c.setProperty("socket.txBufSize", "4096"); + tomcat.start(); + + SimpleAjpClient ajpClient = new SimpleAjpClient(); + ajpClient.setPort(getPort()); + ajpClient.connect(); + + TestAbstractAjpProcessor.validateCpong(ajpClient.cping()); + + ajpClient.setUri("/"); + TesterAjpMessage forwardMessage = ajpClient.createForwardMessage(); + forwardMessage.end(); + + Thread.sleep(1000); + + // Expected headers + TesterAjpMessage responseHeaders = ajpClient.sendMessage(forwardMessage); + TestAbstractAjpProcessor.validateResponseHeaders(responseHeaders, 200, "200"); + + TesterAjpMessage msg = ajpClient.readMessage(); + byte type = msg.getByte(); + int responseByteCount = 0; + while (type == 0x03) { + responseByteCount += msg.getInt(); + msg = ajpClient.readMessage(); + type = msg.getByte(); + } + + Assert.assertTrue("Unexpected message type [" + HexUtils.toHexString(new byte[] { type }) + "]", type == 0x05); + Assert.assertEquals("Incomplete response body", LargePayloadServlet.DEFAULT_SIZE, responseByteCount); + ajpClient.disconnect(); + } + + + static final class LargePayloadServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_SIZE = 1_382_300; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + int size = DEFAULT_SIZE; + String requested = req.getParameter("size"); + if (requested != null) { + size = Integer.parseInt(requested); + } + byte[] payload = buildPayload(size); + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.setContentLengthLong(payload.length); + + AsyncContext async = req.startAsync(); + ServletOutputStream out = resp.getOutputStream(); + out.setWriteListener(new WriteListener() { + private boolean written; + + @Override + public void onWritePossible() throws IOException { + if (!written) { + written = true; + // One write of the whole array. Tomcat splits it into AJP packets and + // reaches the Content-Length on the final one, triggering the auto-close + // from inside this call. + out.write(payload); + } + if (out.isReady()) { + async.complete(); + } else { + System.out.println("onWritePossible() NOT READY"); + } + } + + @Override + public void onError(Throwable t) { + async.complete(); + } + }); + } + + /** Deterministic filler with a recognisable JSON prefix. */ + private static byte[] buildPayload(int size) { + byte[] out = new byte[size]; + byte[] prefix = "{\"payload\":\"".getBytes(StandardCharsets.UTF_8); + System.arraycopy(prefix, 0, out, 0, Math.min(prefix.length, size)); + for (int i = prefix.length; i < size - 2; i++) { + out[i] = (byte) ('a' + ((i - prefix.length) % 26)); + } + if (size >= 2) { + out[size - 2] = '"'; + out[size - 1] = '}'; + } + return out; + } + } + +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
