From 59ca067cb281d472663efa74fa0e816bbda66113 Mon Sep 17 00:00:00 2001 From: Joe Orton Date: Fri, 9 Oct 2026 19:37:21 +0000 Subject: [PATCH] mod_sed: Fix handling of malformed scripts: * modules/filters/sed0.c (fcomp): Reject { nested more than SED_DEPTH deep, and an s or y command with no delimiter. (ycomp): Index the translation table by the unsigned value of each byte. (comple): Return NULL from a failed compile without storing circf. (address): Reject a line number too large for apr_int64_t. * test/modules/filters/test_003_sed.py (test_filters_003_37, test_filters_003_38, test_filters_003_42, test_filters_003_43, test_filters_003_44, test_filters_003_46, test_filters_003_47): New tests. Co-Authored-By: Claude Opus 5.5 GitHub: PR #791 git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1938965 13f79535-47bb-0310-9956-ffa450edef68 --- modules/filters/sed0.c | 25 +++++++++++++-- test/modules/filters/test_003_sed.py | 48 ++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/modules/filters/sed0.c b/modules/filters/sed0.c index 84ed02d4e9..003fe31bc9 100644 --- a/modules/filters/sed0.c +++ b/modules/filters/sed0.c @@ -295,6 +295,10 @@ swit: case '{': commands->rep->command = BCOM; commands->rep->negfl = !(commands->rep->negfl); + if (commands->depth >= SED_DEPTH) { + command_errf(commands, SEDERR_TMOMES); + return -1; + } commands->cmpend[commands->depth++] = &commands->rep->lb1; commands->rep = alloc_reptr(commands); commands->rep->ad1 = p; @@ -528,6 +532,10 @@ jtcommon: case 's': commands->rep->command = SCOM; + if (*commands->cp == '\0') { + command_errf(commands, SEDERR_CGMES, commands->linebuf); + return -1; + } commands->sseof = *commands->cp++; commands->rep->re1 = p; p = comple(commands, &compargs, (char *) 0, commands->rep->re1, @@ -645,6 +653,10 @@ jtcommon: case 'y': commands->rep->command = YCOM; + if (*commands->cp == '\0') { + command_errf(commands, SEDERR_CGMES, commands->linebuf); + return -1; + } commands->sseof = *commands->cp++; commands->rep->re1 = p; p = ycomp(commands, commands->rep->re1); @@ -839,8 +851,13 @@ static char *address(sed_commands_t *commands, char *expbuf, rcp = commands->cp; lno = 0; - while(*rcp >= '0' && *rcp <= '9') - lno = lno*10 + *rcp++ - '0'; + while(*rcp >= '0' && *rcp <= '9') { + if (lno > (APR_INT64_MAX - (*rcp - '0')) / 10) { + *status = APR_EGENERAL; + return NULL; + } + lno = lno*10 + (*rcp++ - '0'); + } if(rcp > commands->cp) { if (expbuf > &commands->respace[RESIZE-3]) { @@ -954,7 +971,7 @@ static char *ycomp(sed_commands_t *commands, char *expbuf) sp++; c = '\n'; } - cint = (int) c; + cint = (unsigned char) c; if((ep[cint] = *tsp++) == '\\' && *tsp == 'n') { ep[cint] = '\n'; tsp++; @@ -996,6 +1013,8 @@ static char *comple(sed_commands_t *commands, sed_comp_args *compargs, char *p; p = sed_compile(commands, compargs, ep + 1, x3, x4); + if(p == NULL) + return NULL; if(p == ep + 1) return(ep); *ep = compargs->circf; diff --git a/test/modules/filters/test_003_sed.py b/test/modules/filters/test_003_sed.py index a02804ff52..ab6b76e9bb 100644 --- a/test/modules/filters/test_003_sed.py +++ b/test/modules/filters/test_003_sed.py @@ -452,6 +452,19 @@ class TestSed: assert self.body(env, r"s/\(\(mo\)n\2*\)day/[\2]/").decode() == \ "one [mo] two\nthree sunday four\n[mo] monday monday\n" + # --- blocks ----------------------------------------------------------- + + # Blocks nest up to 20 deep. Each } goes in its own directive: the + # rest of a line after } is not read. + def test_filters_003_37(self, env): + assert self.body(env, ["{" * 20 + "s/monday/MON/"] + ["}"] * 20) \ + .decode() == "one MON two\nthree sunday four\nMON monday monday\n" + + # Any deeper is rejected. + def test_filters_003_38(self, env): + self.configure(env, ["{" * 21 + "p"] + ["}"] * 21, fails=True) + assert "too many {'s" in env.apachectl_stderr, env.apachectl_stderr + # --- bracket expressions ---------------------------------------------- # A bracket expression which ends after a range's "-" is rejected, on @@ -486,6 +499,30 @@ class TestSed: b"\\\xe9sunday\xe9s\xe9u\xe9U\xe9"]).decode() \ == "one MON two\nthree sUnday four\nMON monday monday\n" + # An s or y command which ends before its delimiter is rejected. + @pytest.mark.parametrize("expr", ["s", "/monday/s", "y", "/monday/y"]) + def test_filters_003_42(self, env, expr): + self.configure(env, expr, fails=True) + assert "command garbled" in env.apachectl_stderr, \ + env.apachectl_stderr + + # y maps bytes with the high bit set like any other. + def test_filters_003_43(self, env): + self.write_bytes(env, "latin1.html", b"caf\xe9 na\xefve\n") + assert self.body(env, [b"y/\xe9\xef/ei/"], path="/latin1.html") == \ + b"cafe naive\n" + + # A regular expression which no longer fits once the compiled script + # has filled its buffer exactly, here with the names of r commands, is + # rejected without writing past the buffer. Only a memory checker sees + # the difference. + def test_filters_003_44(self, env): + self.configure(env, [f"r {'A' * 998}"] * 10 + ["r 123456789", + "/x/p"], + fails=True) + assert "too much command text" in env.apachectl_stderr, \ + env.apachectl_stderr + # An interval count is rejected as too large however many digits it # has, rather than wrapping around. @pytest.mark.parametrize("expr", [ @@ -497,3 +534,14 @@ class TestSed: self.configure(env, expr, fails=True) assert "Range endpoint too large" in env.apachectl_stderr, \ env.apachectl_stderr + + # A line number too large for a 64-bit count is rejected rather than + # wrapping around; one which fits, but which no document reaches, is + # accepted. + def test_filters_003_46(self, env): + self.configure(env, "99999999999999999999p", fails=True) + assert "command garbled" in env.apachectl_stderr, \ + env.apachectl_stderr + + def test_filters_003_47(self, env): + assert self.body(env, "9223372036854775807d").decode() == DOC