]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
mod_proxy_ajp: Add protocol robustness tests
authorGiannis Christodoulou <ichristod@apache.org>
Thu, 4 Jun 2026 07:32:47 +0000 (07:32 +0000)
committerGiannis Christodoulou <ichristod@apache.org>
Thu, 4 Jun 2026 07:32:47 +0000 (07:32 +0000)
Add coverage for AJP message size limits, header length handling,
and chunk message handling.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1934964 13f79535-47bb-0310-9956-ffa450edef68

test/modules/proxy/test_04_ajp.py [new file with mode: 0644]

diff --git a/test/modules/proxy/test_04_ajp.py b/test/modules/proxy/test_04_ajp.py
new file mode 100644 (file)
index 0000000..fabc8ec
--- /dev/null
@@ -0,0 +1,126 @@
+import pytest
+import re
+
+from pyhttpd.conf import HttpdConf
+from .env import TCPFaker
+
+magic_numbers = b"\x41\x42"
+send_headers_type = b"\x04"
+send_chunk_type = b"\x03"
+status_code_200 = b"\x00\xc8"
+status_msg_ok = b"OK"
+ajp_max_size = 8192
+
+
+def ajp_packet_from_tomcat(body):
+    return magic_numbers + len(body).to_bytes(2, "big") + body
+
+
+class _AJPFaker(TCPFaker):
+
+    @staticmethod
+    def max_size_message(data):
+        body = send_headers_type + b"\x00" * (ajp_max_size - 1)
+        return ajp_packet_from_tomcat(body)
+
+    @staticmethod
+    def num_headers_len(data):
+        # message_type(send_headers) + status_code_200 + len(inner_body)
+        # + "OK"
+        body = (send_headers_type
+                + status_code_200
+                + len(status_msg_ok).to_bytes(2, "big")
+                + status_msg_ok
+                + b"\x00")
+        return ajp_packet_from_tomcat(body)
+
+    @staticmethod
+    def missing_null_terminator(data):
+        # message_type(send_headers) + status_code_200 + len(inner_body)
+        # + "OK" without null terminator
+        body = (send_headers_type
+                + status_code_200
+                + len(status_msg_ok).to_bytes(2, "big")
+                + status_msg_ok)
+        return ajp_packet_from_tomcat(body)
+
+    @staticmethod
+    def tiny_body_chunk(data):
+        # message_type(send_body_chunk) + chunk_size(0xffff),3 bytes total
+        chunk_body = (send_chunk_type + b"\xff\xff")
+        return ajp_packet_from_tomcat(chunk_body)
+
+
+class TestProxyAjp:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        if not env.has_shared_module("proxy_ajp"):
+            pytest.skip("mod_proxy_ajp not available")
+        faker = _AJPFaker("127.0.0.1", env.http_port2)
+        faker.start()
+        conf = HttpdConf(env)
+        conf.start_vhost(domains=[f"test1.{env.http_tld}"], port=env.http_port)
+        conf.add([
+            f"ProxyPass / ajp://127.0.0.1:{env.http_port2}/",
+        ])
+        conf.end_vhost()
+        conf.install()
+        assert env.apache_restart() == 0
+        yield faker
+        faker.stop()
+
+    def test_proxy_004_01(self, env, _class_scope):
+        _class_scope._make_response = _AJPFaker.max_size_message
+        r = env.curl_get(env.mkurl("http", "test1", "/"))
+        assert r.response["status"] == 503
+        try:
+            found = env.httpd_error_log.scan_recent(
+                re.compile(r'.*AH01081:.*'), timeout=5)
+        except TimeoutError:
+            found = False
+
+        assert found
+        env.httpd_error_log.ignore_recent(
+            lognos=["AH01081", "AH01080", "AH01031", "AH00878", "AH00992"])
+
+    def test_proxy_004_02(self, env, _class_scope):
+        _class_scope._make_response = _AJPFaker.tiny_body_chunk
+        env.curl_get(env.mkurl("http", "test1", "/"))
+        try:
+            found = env.httpd_error_log.scan_recent(
+                re.compile(r'.*ajp_parse_data: Message too small.*'),
+                timeout=5)
+        except TimeoutError:
+            found = False
+
+        assert found
+        env.httpd_error_log.ignore_recent(
+            lognos=["AH00893"],
+            matches=[r'.*ajp_parse_data: Message too small.*'])
+
+    def test_proxy_004_03(self, env, _class_scope):
+        _class_scope._make_response = _AJPFaker.num_headers_len
+        r = env.curl_get(env.mkurl("http", "test1", "/"))
+        assert r.exit_code == 0
+
+        _class_scope._make_response = _AJPFaker.missing_null_terminator
+        r = env.curl_get(env.mkurl("http", "test1", "/"))
+        assert r.exit_code == 0
+
+        try:
+            found = env.httpd_error_log.scan_recent(
+                re.compile(r'.*AH03229: ajp_msg_get_uint16.*'), timeout=5)
+        except TimeoutError:
+            found = False
+        assert found
+
+        try:
+            found = env.httpd_error_log.scan_recent(
+                re.compile(r'.*AH03229: ajp_msg_get_string.*'), timeout=5)
+        except TimeoutError:
+            found = False
+        assert found
+
+        env.httpd_error_log.ignore_recent(
+            lognos=["AH03229", "AH10405", "AH00985", "AH00893"])