From 821b7c2d94cd551ac03aed36b5cce5b3500ad047 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 19 Jul 2026 00:29:51 -0300 Subject: [PATCH 1/3] Add opt-in build flag to match v2's @rx anchor semantics libmodsecurity's shared Regex class compiles all patterns with PCRE(2)_MULTILINE, while ModSecurity v2's @rx operator uses PCRE(2)_DOLLAR_ENDONLY, so ^/$ anchor differently against multi-line values between the two engines (owasp-modsecurity/ModSecurity#3295). Add --enable-regex-dollar-endonly, off by default, mirroring the build-flag rollout Coraza used for the same issue (corazawaf/coraza#876). When enabled, only @rx and @rxGlobal compile with DOLLAR_ENDONLY instead of MULTILINE; every other Regex caller (verifyCC, verifySSN, verifyCPF, internal chain/audit-log regexes) keeps its current behavior, matching how v2 itself only applies DOLLAR_ENDONLY to @rx. Co-Authored-By: Claude Sonnet 5 --- README.md | 5 +++++ configure.ac | 5 +++++ src/operators/rx.cc | 9 +++++++-- src/operators/rx_global.cc | 9 +++++++-- src/utils/regex.cc | 7 ++++--- src/utils/regex.h | 3 ++- 6 files changed, 30 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7bb6a0aa95..300fda1c35 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,11 @@ Windows build information can be found [here](build/win32/README.md). * **Default:** PCRE2 is detected and used. * **Fallback:** legacy PCRE can be used if `--with-pcre` is explicitly provided (`WITH_PCRE`). * In other words, current builds expect PCRE2 unless explicitly configured otherwise. +* `@rx` and `@rxGlobal` compile patterns with `DOTALL | MULTILINE` by default. Pass + `--enable-regex-dollar-endonly=yes` to compile them with `DOTALL | DOLLAR_ENDONLY` + instead, matching ModSecurity v2's `@rx` behavior (`^`/`$` anchor only at the + start/end of the whole subject, not at internal line breaks). This flag is + disabled by default; see [issue #3295](https://github.com/owasp-modsecurity/ModSecurity/issues/3295). All other dependencies are related to operators specified within SecRules or configuration directives and may not be required for compilation. diff --git a/configure.ac b/configure.ac index 7bdcca6bc4..92f8438a79 100644 --- a/configure.ac +++ b/configure.ac @@ -285,6 +285,11 @@ MSC_ARG_ENABLE_BOOL([afl-fuzz], [Turn on the afl fuzzer compilation utilities], MSC_ARG_ENABLE_BOOL([examples], [Turn on the examples compilation (default option)], [true], [buildExamples]) MSC_ARG_ENABLE_BOOL([parser-generation], [Enables parser generation during the build], [false], [buildParser]) MSC_ARG_ENABLE_BOOL([mutex-on-pm], [Treat pm operations as critical section], [false], [mutexPm]) +MSC_ARG_ENABLE_BOOL([regex-dollar-endonly], [Compile @rx/@rxGlobal patterns with PCRE(2)_DOLLAR_ENDONLY instead of PCRE(2)_MULTILINE, matching ModSecurity v2 behavior (see issue #3295)], [false], [regexDollarEndonly]) +if test "$regexDollarEndonly" = "true"; then + MODSEC_REGEX_DOLLAR_ENDONLY_CPPFLAGS="-DMODSEC_REGEX_DOLLAR_ENDONLY=1" + GLOBAL_CPPFLAGS="$GLOBAL_CPPFLAGS $MODSEC_REGEX_DOLLAR_ENDONLY_CPPFLAGS" +fi if test $buildParser = true; then diff --git a/src/operators/rx.cc b/src/operators/rx.cc index de1428e2e5..2fe8ae2a1d 100644 --- a/src/operators/rx.cc +++ b/src/operators/rx.cc @@ -26,10 +26,15 @@ namespace modsecurity { namespace operators { +#ifdef MODSEC_REGEX_DOLLAR_ENDONLY +static const bool kRxMultiLine = false; +#else +static const bool kRxMultiLine = true; +#endif bool Rx::init(const std::string &arg, std::string *error) { if (m_string->m_containsMacro == false) { - m_re = new Regex(m_param); + m_re = new Regex(m_param, false, kRxMultiLine); } return true; @@ -46,7 +51,7 @@ bool Rx::evaluate(Transaction *transaction, RuleWithActions *rule, if (m_string->m_containsMacro) { std::string eparam(m_string->evaluate(transaction)); - re = new Regex(eparam); + re = new Regex(eparam, false, kRxMultiLine); } else { re = m_re; } diff --git a/src/operators/rx_global.cc b/src/operators/rx_global.cc index a966ed35b3..9a1b5005f2 100644 --- a/src/operators/rx_global.cc +++ b/src/operators/rx_global.cc @@ -26,10 +26,15 @@ namespace modsecurity { namespace operators { +#ifdef MODSEC_REGEX_DOLLAR_ENDONLY +static const bool kRxGlobalMultiLine = false; +#else +static const bool kRxGlobalMultiLine = true; +#endif bool RxGlobal::init(const std::string &arg, std::string *error) { if (m_string->m_containsMacro == false) { - m_re = new Regex(m_param); + m_re = new Regex(m_param, false, kRxGlobalMultiLine); } return true; @@ -46,7 +51,7 @@ bool RxGlobal::evaluate(Transaction *transaction, RuleWithActions *rule, if (m_string->m_containsMacro) { std::string eparam(m_string->evaluate(transaction)); - re = new Regex(eparam); + re = new Regex(eparam, false, kRxGlobalMultiLine); } else { re = m_re; } diff --git a/src/utils/regex.cc b/src/utils/regex.cc index 3002503743..42b3018bb1 100644 --- a/src/utils/regex.cc +++ b/src/utils/regex.cc @@ -87,11 +87,12 @@ bool crlfIsNewline() { return crlf_is_newline; } -Regex::Regex(const std::string& pattern_, bool ignoreCase) +Regex::Regex(const std::string& pattern_, bool ignoreCase, bool multiLine) : pattern(pattern_.empty() ? ".*" : pattern_) { #ifndef WITH_PCRE PCRE2_SPTR pcre2_pattern = reinterpret_cast(pattern.c_str()); - uint32_t pcre2_options = (PCRE2_DOTALL|PCRE2_MULTILINE); + uint32_t pcre2_options = PCRE2_DOTALL | + (multiLine ? PCRE2_MULTILINE : PCRE2_DOLLAR_ENDONLY); if (ignoreCase) { pcre2_options |= PCRE2_CASELESS; } @@ -103,7 +104,7 @@ Regex::Regex(const std::string& pattern_, bool ignoreCase) #else const char *errptr = nullptr; int erroffset; - int flags = (PCRE_DOTALL|PCRE_MULTILINE); + int flags = PCRE_DOTALL | (multiLine ? PCRE_MULTILINE : PCRE_DOLLAR_ENDONLY); if (ignoreCase == true) { flags |= PCRE_CASELESS; diff --git a/src/utils/regex.h b/src/utils/regex.h index 863ce560b6..62981c70c9 100644 --- a/src/utils/regex.h +++ b/src/utils/regex.h @@ -71,7 +71,8 @@ struct SMatchCapture { class Regex { public: - explicit Regex(const std::string& pattern_, bool ignoreCase = false); + explicit Regex(const std::string& pattern_, bool ignoreCase = false, + bool multiLine = true); ~Regex(); // m_pc and m_pce can't be easily copied From d595832f91bd153a4104ddc45d3a91260766d415 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 19 Jul 2026 11:57:08 -0300 Subject: [PATCH 2/3] Address Copilot review feedback on PR #3600 - README: clarify that MULTILINE controls where ^/$ can anchor (per-line vs. whole-subject), while DOLLAR_ENDONLY only further refines $ against a trailing newline; DOTALL is unchanged either way so . still matches across internal line breaks. - Add test/unit/regex_multiline_test.cc, a standalone regression test for the multiLine constructor parameter added to Utils::Regex, covering both the multiLine=true (default) and multiLine=false paths. Wired into test/Makefile.am as its own noinst_PROGRAMS binary and into test/test-suite.in / test-suite.sh (the existing dispatcher only knew how to run JSON test-case files against unit_tests/regression_tests). Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + README.md | 13 +++-- test/Makefile.am | 26 ++++++++++ test/test-suite.in | 1 + test/test-suite.sh | 10 +++- test/unit/regex_multiline_test.cc | 82 +++++++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 test/unit/regex_multiline_test.cc diff --git a/.gitignore b/.gitignore index 4e314e466e..ab34d654e6 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ src/headers.mk /test/rules_optimization /test/regression_tests /test/unit_tests +/test/regex_multiline_test /test-driver /test/massif.out.* /test/benchmark/benchmark diff --git a/README.md b/README.md index 300fda1c35..bbbd90bb10 100644 --- a/README.md +++ b/README.md @@ -115,11 +115,16 @@ Windows build information can be found [here](build/win32/README.md). * **Default:** PCRE2 is detected and used. * **Fallback:** legacy PCRE can be used if `--with-pcre` is explicitly provided (`WITH_PCRE`). * In other words, current builds expect PCRE2 unless explicitly configured otherwise. -* `@rx` and `@rxGlobal` compile patterns with `DOTALL | MULTILINE` by default. Pass +* `@rx` and `@rxGlobal` compile patterns with `DOTALL | MULTILINE` by default: `DOTALL` + lets `.` match newlines, and `MULTILINE` lets `^`/`$` anchor at every internal line + break rather than only at the start/end of the whole subject. Pass `--enable-regex-dollar-endonly=yes` to compile them with `DOTALL | DOLLAR_ENDONLY` - instead, matching ModSecurity v2's `@rx` behavior (`^`/`$` anchor only at the - start/end of the whole subject, not at internal line breaks). This flag is - disabled by default; see [issue #3295](https://github.com/owasp-modsecurity/ModSecurity/issues/3295). + instead: without `MULTILINE`, `^`/`$` anchor only at the start/end of the whole + subject, and `DOLLAR_ENDONLY` further restricts `$` to match only there rather than + also just before a trailing newline. `DOTALL` is unchanged either way, so patterns + can still match across internal line breaks via `.`. This matches ModSecurity v2's + `@rx` behavior. The flag is disabled by default; see + [issue #3295](https://github.com/owasp-modsecurity/ModSecurity/issues/3295). All other dependencies are related to operators specified within SecRules or configuration directives and may not be required for compilation. diff --git a/test/Makefile.am b/test/Makefile.am index de8d4f4af4..9ab1112929 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -70,6 +70,32 @@ unit_tests_LDFLAGS = \ $(YAJL_LDFLAGS) +# regex_multiline_test +# +# Direct coverage for the Regex(pattern, ignoreCase, multiLine) parameter +# behind --enable-regex-dollar-endonly, independent of the rest of the +# SecRules pipeline exercised by unit_tests/regression_tests. + +noinst_PROGRAMS += regex_multiline_test +regex_multiline_test_SOURCES = \ + unit/regex_multiline_test.cc + +regex_multiline_test_LDFLAGS = \ + -L$(top_builddir)/src/.libs/ \ + -lmodsecurity \ + -lpthread \ + -lm \ + -lstdc++ + +regex_multiline_test_CPPFLAGS = \ + -I$(top_srcdir)/ \ + -g \ + -I$(top_builddir)/headers \ + $(GLOBAL_CPPFLAGS) \ + $(PCRE_CFLAGS) \ + $(PCRE2_CFLAGS) + + unit_tests_CPPFLAGS = \ -Icommon \ -I$(top_srcdir)/ \ diff --git a/test/test-suite.in b/test/test-suite.in index ebda49fb81..61284978ac 100644 --- a/test/test-suite.in +++ b/test/test-suite.in @@ -1,4 +1,5 @@ # for i in `find test/test-cases -iname *.json`; do echo TESTS+=$i; done +TESTS+=test/unit/regex_multiline_test TESTS+=test/test-cases/regression/action-allow.json TESTS+=test/test-cases/regression/action-block.json TESTS+=test/test-cases/regression/action-ctl_request_body_access.json diff --git a/test/test-suite.sh b/test/test-suite.sh index 20262239d7..217933809f 100755 --- a/test/test-suite.sh +++ b/test/test-suite.sh @@ -8,7 +8,15 @@ array=${@:1:$length} PARAM=$array FILE=${@: -1} -if [[ $FILE == *"test-cases/regression/"* ]] +if [[ $FILE != *.json ]] +then + # Self-contained test binary (not a JSON test-case file) - run it directly. + $VALGRIND $PARAM ./$(basename $FILE) + RET=$? + if [ $RET -ne 0 ]; then + echo ":test-result: FAIL: ../$FILE" + fi +elif [[ $FILE == *"test-cases/regression/"* ]] then AMOUNT=$(./regression_tests countall ../$FILE) RET=$? diff --git a/test/unit/regex_multiline_test.cc b/test/unit/regex_multiline_test.cc new file mode 100644 index 0000000000..62ce727c5d --- /dev/null +++ b/test/unit/regex_multiline_test.cc @@ -0,0 +1,82 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + * Copyright (c) 2015 - 2021 Trustwave Holdings, Inc. (http://www.trustwave.com/) + * + * 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 + * + * If any of the files related to licensing are missing or if you have any + * other questions related to licensing please contact Trustwave Holdings, Inc. + * directly using the email address security@modsecurity.org. + * + */ + +#include +#include + +#include "src/utils/regex.h" + +using modsecurity::Utils::Regex; + +namespace { + +int g_failures = 0; + +void check(const std::string &label, const Regex &re, const std::string &input, + bool expectMatch) { + const bool matched = re.search(input) > 0; + if (matched != expectMatch) { + std::cerr << "FAIL: " << label << " - expected " + << (expectMatch ? "match" : "no match") << ", got " + << (matched ? "match" : "no match") << std::endl; + g_failures++; + } else { + std::cout << "PASS: " << label << std::endl; + } +} + +} // namespace + +/* + * Regression coverage for the Regex(pattern, ignoreCase, multiLine) parameter + * added to support --enable-regex-dollar-endonly (see README.md and + * https://github.com/owasp-modsecurity/ModSecurity/issues/3295). Every other + * Regex call site in the codebase relies on the multiLine=true default and is + * unaffected by this parameter. + */ +int main() { + const std::string pattern = "^hello.*world$"; + const std::string multiLineInput = "test\nhello world\nmore"; + + /* multiLine=true (default): PCRE2_MULTILINE / PCRE_MULTILINE, so ^/$ + * anchor at internal line breaks and the pattern matches the middle + * line on its own. */ + check("multiLine=true matches across internal line breaks", + Regex(pattern, false, true), multiLineInput, true); + + /* multiLine=false: PCRE2_DOLLAR_ENDONLY / PCRE_DOLLAR_ENDONLY, so ^/$ + * anchor only at the start/end of the whole subject, matching + * ModSecurity v2's @rx behavior. */ + check("multiLine=false does not match across internal line breaks", + Regex(pattern, false, false), multiLineInput, false); + + check("multiLine=false still matches a single-line subject", + Regex(pattern, false, false), "hello world", true); + + /* DOLLAR_ENDONLY specifically makes '$' match only at the true end of + * the subject, not just before a trailing newline. */ + check("multiLine=false rejects a trailing newline before '$'", + Regex("123$", false, false), "123\n", false); + check("multiLine=false matches '$' at the true end of the subject", + Regex("123$", false, false), "123", true); + + if (g_failures == 0) { + std::cout << "All Regex multiLine tests passed." << std::endl; + } else { + std::cout << g_failures << " Regex multiLine test(s) failed." << std::endl; + } + + return g_failures; +} From 08d0afc02674390494fc62eb78855ef1667b5cea Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Sun, 19 Jul 2026 16:46:09 -0300 Subject: [PATCH 3/3] Use JSON regression test with a build-gated resource instead of a standalone test binary Per @airween's review feedback: the regression test framework already supports skipping a JSON test case at runtime via a "resource" key (test/regression/regression.cc), matching it against a list of compile-time-enabled features (WITH_CURL, WITH_LUA, WITH_LIBXML2, etc.). Register "regex-dollar-endonly" the same way under MODSEC_REGEX_DOLLAR_ENDONLY, and add test/test-cases/regression/operator-rx-dollar-endonly.json gated on it, covering both a single-line match (still blocks) and a multi-line ARGS value (anchors no longer span line breaks). Builds without the flag skip these two cases; builds with it pass. This replaces the standalone regex_multiline_test.cc binary and its Makefile.am/test-suite.in/test-suite.sh wiring from the previous commit, which needlessly diverged from how every other test in this project is structured. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 - test/Makefile.am | 26 ----- test/regression/regression.cc | 3 + .../operator-rx-dollar-endonly.json | 94 +++++++++++++++++++ test/test-suite.in | 2 +- test/test-suite.sh | 10 +- test/unit/regex_multiline_test.cc | 82 ---------------- 7 files changed, 99 insertions(+), 119 deletions(-) create mode 100644 test/test-cases/regression/operator-rx-dollar-endonly.json delete mode 100644 test/unit/regex_multiline_test.cc diff --git a/.gitignore b/.gitignore index ab34d654e6..4e314e466e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,6 @@ src/headers.mk /test/rules_optimization /test/regression_tests /test/unit_tests -/test/regex_multiline_test /test-driver /test/massif.out.* /test/benchmark/benchmark diff --git a/test/Makefile.am b/test/Makefile.am index 9ab1112929..de8d4f4af4 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -70,32 +70,6 @@ unit_tests_LDFLAGS = \ $(YAJL_LDFLAGS) -# regex_multiline_test -# -# Direct coverage for the Regex(pattern, ignoreCase, multiLine) parameter -# behind --enable-regex-dollar-endonly, independent of the rest of the -# SecRules pipeline exercised by unit_tests/regression_tests. - -noinst_PROGRAMS += regex_multiline_test -regex_multiline_test_SOURCES = \ - unit/regex_multiline_test.cc - -regex_multiline_test_LDFLAGS = \ - -L$(top_builddir)/src/.libs/ \ - -lmodsecurity \ - -lpthread \ - -lm \ - -lstdc++ - -regex_multiline_test_CPPFLAGS = \ - -I$(top_srcdir)/ \ - -g \ - -I$(top_builddir)/headers \ - $(GLOBAL_CPPFLAGS) \ - $(PCRE_CFLAGS) \ - $(PCRE2_CFLAGS) - - unit_tests_CPPFLAGS = \ -Icommon \ -I$(top_srcdir)/ \ diff --git a/test/regression/regression.cc b/test/regression/regression.cc index 5b1ca514e8..2f2da5ebfb 100644 --- a/test/regression/regression.cc +++ b/test/regression/regression.cc @@ -430,6 +430,9 @@ int main(int argc, char **argv) #ifdef WITH_LIBXML2 resources.push_back("libxml2"); #endif +#ifdef MODSEC_REGEX_DOLLAR_ENDONLY + resources.push_back("regex-dollar-endonly"); +#endif #ifdef NO_LOGS std::cout << "Test utility cannot work without logging support." \ diff --git a/test/test-cases/regression/operator-rx-dollar-endonly.json b/test/test-cases/regression/operator-rx-dollar-endonly.json new file mode 100644 index 0000000000..08b2793b30 --- /dev/null +++ b/test/test-cases/regression/operator-rx-dollar-endonly.json @@ -0,0 +1,94 @@ +[ + { + "enabled": 1, + "version_min": 300000, + "resource": "regex-dollar-endonly", + "title": "Testing Operator :: @rx --enable-regex-dollar-endonly (single-line match still blocks)", + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "0" + }, + "uri": "/?param1=hello%20world", + "method": "GET", + "body": [ + "" + ] + }, + "response": { + "headers": { + "Date": "Mon, 13 Jul 2015 20:02:41 GMT", + "Last-Modified": "Sun, 26 Oct 2014 22:33:37 GMT", + "Content-Type": "text/html", + "Content-Length": "8" + }, + "body": [ + "no need." + ] + }, + "expected": { + "debug_log": "Executing operator \"Rx\"", + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS:param1 \"^hello.*world$\" \"id:1,phase:2,deny,log,msg:'multiline-anchor-test'\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "resource": "regex-dollar-endonly", + "title": "Testing Operator :: @rx --enable-regex-dollar-endonly (anchors do not span internal line breaks)", + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "0" + }, + "uri": "/?param1=test%0Ahello%20world%0Amore", + "method": "GET", + "body": [ + "" + ] + }, + "response": { + "headers": { + "Date": "Mon, 13 Jul 2015 20:02:41 GMT", + "Last-Modified": "Sun, 26 Oct 2014 22:33:37 GMT", + "Content-Type": "text/html", + "Content-Length": "8" + }, + "body": [ + "no need." + ] + }, + "expected": { + "debug_log": "Executing operator \"Rx\"", + "http_code": 200 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS:param1 \"^hello.*world$\" \"id:1,phase:2,deny,log,msg:'multiline-anchor-test'\"" + ] + } +] diff --git a/test/test-suite.in b/test/test-suite.in index 61284978ac..ffc4e486ed 100644 --- a/test/test-suite.in +++ b/test/test-suite.in @@ -1,5 +1,4 @@ # for i in `find test/test-cases -iname *.json`; do echo TESTS+=$i; done -TESTS+=test/unit/regex_multiline_test TESTS+=test/test-cases/regression/action-allow.json TESTS+=test/test-cases/regression/action-block.json TESTS+=test/test-cases/regression/action-ctl_request_body_access.json @@ -89,6 +88,7 @@ TESTS+=test/test-cases/regression/operator-ipMatchFromFile.json TESTS+=test/test-cases/regression/operator-pm.json TESTS+=test/test-cases/regression/operator-pmfromfile.json TESTS+=test/test-cases/regression/operator-rx.json +TESTS+=test/test-cases/regression/operator-rx-dollar-endonly.json TESTS+=test/test-cases/regression/operator-rxGlobal.json TESTS+=test/test-cases/regression/operator-UnconditionalMatch.json TESTS+=test/test-cases/regression/operator-validate-byte-range.json diff --git a/test/test-suite.sh b/test/test-suite.sh index 217933809f..20262239d7 100755 --- a/test/test-suite.sh +++ b/test/test-suite.sh @@ -8,15 +8,7 @@ array=${@:1:$length} PARAM=$array FILE=${@: -1} -if [[ $FILE != *.json ]] -then - # Self-contained test binary (not a JSON test-case file) - run it directly. - $VALGRIND $PARAM ./$(basename $FILE) - RET=$? - if [ $RET -ne 0 ]; then - echo ":test-result: FAIL: ../$FILE" - fi -elif [[ $FILE == *"test-cases/regression/"* ]] +if [[ $FILE == *"test-cases/regression/"* ]] then AMOUNT=$(./regression_tests countall ../$FILE) RET=$? diff --git a/test/unit/regex_multiline_test.cc b/test/unit/regex_multiline_test.cc deleted file mode 100644 index 62ce727c5d..0000000000 --- a/test/unit/regex_multiline_test.cc +++ /dev/null @@ -1,82 +0,0 @@ -/* - * ModSecurity, http://www.modsecurity.org/ - * Copyright (c) 2015 - 2021 Trustwave Holdings, Inc. (http://www.trustwave.com/) - * - * 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 - * - * If any of the files related to licensing are missing or if you have any - * other questions related to licensing please contact Trustwave Holdings, Inc. - * directly using the email address security@modsecurity.org. - * - */ - -#include -#include - -#include "src/utils/regex.h" - -using modsecurity::Utils::Regex; - -namespace { - -int g_failures = 0; - -void check(const std::string &label, const Regex &re, const std::string &input, - bool expectMatch) { - const bool matched = re.search(input) > 0; - if (matched != expectMatch) { - std::cerr << "FAIL: " << label << " - expected " - << (expectMatch ? "match" : "no match") << ", got " - << (matched ? "match" : "no match") << std::endl; - g_failures++; - } else { - std::cout << "PASS: " << label << std::endl; - } -} - -} // namespace - -/* - * Regression coverage for the Regex(pattern, ignoreCase, multiLine) parameter - * added to support --enable-regex-dollar-endonly (see README.md and - * https://github.com/owasp-modsecurity/ModSecurity/issues/3295). Every other - * Regex call site in the codebase relies on the multiLine=true default and is - * unaffected by this parameter. - */ -int main() { - const std::string pattern = "^hello.*world$"; - const std::string multiLineInput = "test\nhello world\nmore"; - - /* multiLine=true (default): PCRE2_MULTILINE / PCRE_MULTILINE, so ^/$ - * anchor at internal line breaks and the pattern matches the middle - * line on its own. */ - check("multiLine=true matches across internal line breaks", - Regex(pattern, false, true), multiLineInput, true); - - /* multiLine=false: PCRE2_DOLLAR_ENDONLY / PCRE_DOLLAR_ENDONLY, so ^/$ - * anchor only at the start/end of the whole subject, matching - * ModSecurity v2's @rx behavior. */ - check("multiLine=false does not match across internal line breaks", - Regex(pattern, false, false), multiLineInput, false); - - check("multiLine=false still matches a single-line subject", - Regex(pattern, false, false), "hello world", true); - - /* DOLLAR_ENDONLY specifically makes '$' match only at the true end of - * the subject, not just before a trailing newline. */ - check("multiLine=false rejects a trailing newline before '$'", - Regex("123$", false, false), "123\n", false); - check("multiLine=false matches '$' at the true end of the subject", - Regex("123$", false, false), "123", true); - - if (g_failures == 0) { - std::cout << "All Regex multiLine tests passed." << std::endl; - } else { - std::cout << g_failures << " Regex multiLine test(s) failed." << std::endl; - } - - return g_failures; -}