From 9af51f7e69f5981b6b5af03aff3f51840e91d80c Mon Sep 17 00:00:00 2001 From: arshiya tabasum Date: Wed, 30 Sep 2026 12:33:41 +0530 Subject: [PATCH 1/2] mod_speling: avoid out-of-bounds read in check_speling --- changes-entries/speling-uri-underflow.txt | 3 +++ modules/mappers/mod_speling.c | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 changes-entries/speling-uri-underflow.txt diff --git a/changes-entries/speling-uri-underflow.txt b/changes-entries/speling-uri-underflow.txt new file mode 100644 index 00000000000..faf7f8aa9ee --- /dev/null +++ b/changes-entries/speling-uri-underflow.txt @@ -0,0 +1,3 @@ + *) mod_speling: Avoid an out-of-bounds read in the URL/filename check when a + mapped file's base name is longer than the request URI. + [arshiya tabasum ] diff --git a/modules/mappers/mod_speling.c b/modules/mappers/mod_speling.c index 99b8765416a..59688b030eb 100644 --- a/modules/mappers/mod_speling.c +++ b/modules/mappers/mod_speling.c @@ -255,6 +255,17 @@ static int check_speling(request_rec *r) urlen = strlen(r->uri); pglen = strlen(postgood); + /* + * postgood is meant to be a trailing substring of r->uri. When a mapper + * (e.g. Alias or a RewriteRule) points a short URI at a file whose base + * name is longer, postgood is longer than r->uri and urlen - pglen goes + * negative; r->uri + (urlen - pglen) would then read before the start of + * the buffer. Bail out first so the suffix compare stays in bounds. + */ + if (pglen > urlen) { + return DECLINED; + } + /* Check to see if the URL pieces add up */ if (strcmp(postgood, r->uri + (urlen - pglen))) { return DECLINED; From 2084b14b6413eb7110b280541f729cfe351dff68 Mon Sep 17 00:00:00 2001 From: arshiya tabasum Date: Thu, 1 Oct 2026 12:15:58 +0530 Subject: [PATCH 2/2] test: cover check_speling short-URI alias OOB path --- test/pytest_suite/t/conf/extra.conf.in | 4 ++++ test/pytest_suite/tests/t/modules/test_speling.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/test/pytest_suite/t/conf/extra.conf.in b/test/pytest_suite/t/conf/extra.conf.in index 8328632210e..cb3acce9336 100644 --- a/test/pytest_suite/t/conf/extra.conf.in +++ b/test/pytest_suite/t/conf/extra.conf.in @@ -1538,6 +1538,10 @@ LimitRequestFields 32 CheckSpelling on CheckCaseOnly on + # Point a short URI at a longer, nonexistent base name in a CheckSpelling + # directory. This exercised the r->uri + (urlen - pglen) underflow in + # check_speling(); the server must answer cleanly rather than read OOB. + Alias /sp-oob @SERVERROOT@/htdocs/modules/speling/nocase/nonexistent-long-name.html diff --git a/test/pytest_suite/tests/t/modules/test_speling.py b/test/pytest_suite/tests/t/modules/test_speling.py index ce39ce16915..ed677cb9b85 100644 --- a/test/pytest_suite/tests/t/modules/test_speling.py +++ b/test/pytest_suite/tests/t/modules/test_speling.py @@ -59,3 +59,17 @@ def test_speling(http, prefix, code_idx, case): # Only redirect responses carry a corrected-filename body. if expected not in (200, 404): assert t_cmp(r.text, _REDIRECT_BODY), "Redirect ok" + + +@need_module("mod_speling") +def test_speling_short_uri_alias(http): + """Regression for the check_speling() out-of-bounds read. + + ``Alias /sp-oob`` maps a short URI to a longer, nonexistent base name in + a CheckSpelling directory. postgood (the base name) is then longer than + r->uri, so r->uri + (urlen - pglen) underflowed and strcmp read before + the start of the buffer. The server must answer cleanly (404) instead of + crashing or reading out of bounds. + """ + r = http.GET("/sp-oob") + assert t_cmp(r.status_code, 404), "short-URI alias must not read OOB"