diff --git a/test/modules/core/env.py b/test/modules/core/env.py
index 21906be7361..272cb220534 100644
--- a/test/modules/core/env.py
+++ b/test/modules/core/env.py
@@ -12,14 +12,16 @@ class CoreTestSetup(HttpdTestSetup):
def __init__(self, env: 'HttpdTestEnv'):
super().__init__(env=env)
self.add_source_dir(os.path.dirname(inspect.getfile(CoreTestSetup)))
- self.add_modules(["cgid","include","userdir","suexec","headers"])
+ self.add_modules(["cgid","include","userdir","suexec","headers",
+ "actions","asis","info","status","mime",
+ "negotiation"])
class CoreTestEnv(HttpdTestEnv):
def __init__(self, pytestconfig=None):
super().__init__(pytestconfig=pytestconfig)
- self.add_httpd_log_modules(["http", "core"])
+ self.add_httpd_log_modules(["http", "core", "info", "status"])
def setup_httpd(self, setup: HttpdTestSetup = None):
super().setup_httpd(setup=CoreTestSetup(env=self))
diff --git a/test/modules/core/htdocs/cgi/env_parameters.py b/test/modules/core/htdocs/cgi/env_parameters.py
index c08d1b6990d..15766c6e459 100644
--- a/test/modules/core/htdocs/cgi/env_parameters.py
+++ b/test/modules/core/htdocs/cgi/env_parameters.py
@@ -9,6 +9,7 @@
data = {
"REQUEST_METHOD": os.getenv("REQUEST_METHOD", ""),
"QUERY_STRING": os.getenv("QUERY_STRING", ""),
+ "HTTP_PROXY": os.getenv("HTTP_PROXY", ""),
}
print(json.dumps(data, indent=2))
diff --git a/test/modules/core/htdocs/test1/actionstest/dummy.chtml b/test/modules/core/htdocs/test1/actionstest/dummy.chtml
new file mode 100644
index 00000000000..96b0094f5db
--- /dev/null
+++ b/test/modules/core/htdocs/test1/actionstest/dummy.chtml
@@ -0,0 +1 @@
+This is a dummy file for testing mod_actions.
diff --git a/test/modules/core/htdocs/test1/asistest/example.ahtml b/test/modules/core/htdocs/test1/asistest/example.ahtml
new file mode 100644
index 00000000000..5499effe115
--- /dev/null
+++ b/test/modules/core/htdocs/test1/asistest/example.ahtml
@@ -0,0 +1,4 @@
+Status: 301 Moved
+Location: http://example.com/
+
+This has moved.
diff --git a/test/modules/core/htdocs/test1/cgi/handler.py b/test/modules/core/htdocs/test1/cgi/handler.py
new file mode 100755
index 00000000000..b1d36c4e86b
--- /dev/null
+++ b/test/modules/core/htdocs/test1/cgi/handler.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python3
+
+print("Content-Type: text/plain")
+print()
+print("this file was processed")
diff --git a/test/modules/core/test_003_cgi_env_vars.py b/test/modules/core/test_003_cgi_env_vars.py
index f82a5c3764e..a27120e80b4 100644
--- a/test/modules/core/test_003_cgi_env_vars.py
+++ b/test/modules/core/test_003_cgi_env_vars.py
@@ -21,13 +21,19 @@ def _class_scope(self, env):
assert env.apache_restart() == 0
def test_cgi_003_01(self, env):
- """
- CVE-2025-65082:
- Configuration-defined env vars must not override
- server-calculated CGI env vars.
- """
+ """Configuration-defined env vars must not override
+ server-calculated CGI env vars."""
url = env.mkurl("http", "cgi", "/env_parameters.py?x=123")
r = env.curl_get(url)
assert r.response["status"] == 200
assert r.response["json"]["REQUEST_METHOD"] == "GET"
assert r.response["json"]["QUERY_STRING"] == "x=123"
+
+ def test_cgi_003_02(self, env):
+ """A client-supplied Proxy header must not be propagated as
+ HTTP_PROXY to CGI scripts (httpoxy)."""
+ url = env.mkurl("http", "cgi", "/env_parameters.py")
+ r = env.curl_get(url, options=['-H', 'Proxy: http://evil.example.com'])
+ assert r.response["status"] == 200
+ assert r.response["json"]["HTTP_PROXY"] == "", \
+ "HTTP_PROXY should be empty — Proxy header must not leak to CGI"
diff --git a/test/modules/core/test_010_handlers.py b/test/modules/core/test_010_handlers.py
new file mode 100644
index 00000000000..2f0bf4afaf3
--- /dev/null
+++ b/test/modules/core/test_010_handlers.py
@@ -0,0 +1,131 @@
+import os
+import pytest
+
+from pyhttpd.conf import HttpdConf
+
+
+class TestHandlers:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ doc_dir = os.path.join(env.server_dir, "htdocs", "test1")
+ conf = HttpdConf(env, extras={
+ 'base': f"""
+
+ SetHandler server-info
+ Require all granted
+
+
+
+ SetHandler server-status
+ Require all granted
+
+ """,
+ f"test1.{env.http_tld}": f"""
+
+ Options +ExecCGI
+ AddHandler cgi-script .py
+
+ Action html-cgi /cgi/handler.py
+ AddHandler html-cgi .chtml
+ AddHandler send-as-is .ahtml
+ """,
+ })
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ # mod_info: verify server-info handler returns server information page
+ def test_core_010_01(self, env):
+ url = env.mkurl("http", "test1", "/our-server-info")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 200
+ body = r.response["body"].decode()
+ assert "Apache Server Information" in body
+
+ # mod_info: verify ?list query returns module list
+ def test_core_010_02(self, env):
+ url = env.mkurl("http", "test1", "/our-server-info?list")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 200
+ body = r.response["body"].decode()
+ assert "Server Module List" in body
+
+ # mod_status: verify basic server-status page
+ def test_core_010_03(self, env):
+ url = env.mkurl("http", "test1", "/our-server-status")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 200
+ body = r.response["body"].decode()
+ assert "Apache Server Status" in body
+
+ # mod_status: verify extended status shows worker information
+ def test_core_010_04(self, env):
+ # Reconfigure with ExtendedStatus On
+ conf = HttpdConf(env, extras={
+ 'base': f"""
+ ExtendedStatus On
+
+
+ SetHandler server-status
+ Require all granted
+
+ """,
+ })
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ url = env.mkurl("http", "test1", "/our-server-status")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 200
+ body = r.response["body"].decode()
+ assert "requests currently being processed" in body or \
+ "idle workers" in body
+
+ # mod_actions: verify Action directive routes request through CGI handler
+ def test_core_010_05(self, env):
+ # Re-apply the full config (010_04 changed it)
+ doc_dir = os.path.join(env.server_dir, "htdocs", "test1")
+ conf = HttpdConf(env, extras={
+ 'base': f"""
+
+ SetHandler server-info
+ Require all granted
+
+
+ SetHandler server-status
+ Require all granted
+
+ """,
+ f"test1.{env.http_tld}": f"""
+
+ Options +ExecCGI
+ AddHandler cgi-script .py
+
+ Action html-cgi /cgi/handler.py
+ AddHandler html-cgi .chtml
+ AddHandler send-as-is .ahtml
+ """,
+ })
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ url = env.mkurl("http", "test1", "/actionstest/dummy.chtml")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 200
+ body = r.response["body"].decode()
+ assert "this file was processed" in body
+
+ # mod_asis: verify send-as-is handler sends raw response
+ def test_core_010_06(self, env):
+ url = env.mkurl("http", "test1", "/asistest/example.ahtml")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 301
diff --git a/test/modules/core/test_011_userdir.py b/test/modules/core/test_011_userdir.py
new file mode 100644
index 00000000000..6fc715f5bb2
--- /dev/null
+++ b/test/modules/core/test_011_userdir.py
@@ -0,0 +1,42 @@
+import os
+import pytest
+
+from pyhttpd.conf import HttpdConf
+
+
+class TestUserDir:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ # Create userdir content that will be served for any ~user request.
+ # Using UserDir with an absolute path makes every /~user/path resolve
+ # to {absolute_path}/path, avoiding the need for real system users.
+ userdir_base = os.path.join(env.server_dir, "htdocs", "userdir")
+ hello_dir = os.path.join(userdir_base, "anyuser", "hello")
+ os.makedirs(hello_dir, exist_ok=True)
+ with open(os.path.join(hello_dir, "world.txt"), "w") as f:
+ f.write("Hello World!")
+
+ conf = HttpdConf(env, extras={
+ 'base': f"""
+ UserDir "{userdir_base}"
+
+
+ AllowOverride None
+ Require all granted
+
+ """
+ })
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ # UserDir with absolute path: /~anyuser/hello/world.txt serves from
+ # the configured directory regardless of the username
+ def test_core_011_01(self, env):
+ url = env.mkurl("http", "test1", "/~anyuser/hello/world.txt")
+ r = env.curl_get(url)
+ assert r.response, f"no response: {r.stderr}"
+ assert r.response["status"] == 200
+ body = r.response["body"].decode()
+ assert "Hello World!" in body
diff --git a/test/modules/core/test_012_malformed_requests.py b/test/modules/core/test_012_malformed_requests.py
new file mode 100644
index 00000000000..89bb1f83a71
--- /dev/null
+++ b/test/modules/core/test_012_malformed_requests.py
@@ -0,0 +1,85 @@
+import os
+import re
+import pytest
+
+from pyhttpd.conf import HttpdConf
+
+class TestNull:
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = HttpdConf(env)
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_core_012_01(self, env):
+ url = f"https://localhost:{env.http_port}/"
+ env.curl_raw(url, options=[
+ '--connect-timeout', '5', '--max-time', '10', '-k',
+ ])
+
+class TestBadRequest:
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ conf = HttpdConf(env, extras={
+ 'base': """
+ KeepAlive On
+ Redirect 301 /2bad /destination
+ """,
+ })
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_core_012_02(self, env):
+ url = env.mkurl("http", "test1", "/2bad")
+ r = env.curl_raw(url, options=[
+ '-H', 'Content-Length: 1', '-v',
+ '--connect-timeout', '5', '--max-time', '10',
+ ])
+ if r.response is not None:
+ assert r.response["status"] != 301, \
+ "Should not redirect if request body not fully consumed"
+ assert r.response["status"] in [400, 408], \
+ f"Expected 400 or 408, got {r.response['status']}"
+ else:
+ assert "Closing connection" in r.stderr or \
+ "Connection reset" in r.stderr or \
+ r.exit_code != 0, \
+ "Connection should be closed, not left intact"
+ assert "HTTP/1.1 301" not in r.stderr, \
+ "Should not redirect if request body not fully consumed"
+ env.httpd_error_log.ignore_recent(
+ lognos=["AH10390"],
+ matches=[r'.*:error\].*', r'.*:warn\].*'])
+
+
+class TestBadOptions:
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ htdocs = os.path.join(env.server_dir, "htdocs", "test1")
+ ob_dir = os.path.join(htdocs, "badoption")
+ os.makedirs(ob_dir, exist_ok=True)
+ with open(os.path.join(ob_dir, ".htaccess"), "w") as f:
+ f.write("\n\n")
+ conf = HttpdConf(env, extras={
+ f"test1.{env.http_tld}": f"""
+
+ AllowOverride Limit
+
+ """,
+ })
+ conf.add_vhost_test1()
+ conf.install()
+ assert env.apache_restart() == 0
+
+ def test_core_012_03(self, env):
+ """OPTIONS request to directory with invalid Limit directive should
+ return 500 and log a configuration error, not leak memory."""
+ url = env.mkurl("http", "test1", "/badoption/")
+ r = env.curl_raw(url, options=['-X', 'OPTIONS', '-v'])
+ assert r.response is not None
+ assert r.response["status"] == 500
+ env.httpd_error_log.ignore_recent(
+ matches=[r'.*Could not register method.*',
+ r'.*core:error.*'])