diff --git a/README.md b/README.md index 7bb6a0aa9..bbbd90bb1 100644 --- a/README.md +++ b/README.md @@ -115,6 +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: `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: 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/configure.ac b/configure.ac index 7bdcca6bc..92f8438a7 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 de1428e2e..2fe8ae2a1 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 a966ed35b..9a1b5005f 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 300250374..42b3018bb 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 863ce560b..62981c70c 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 diff --git a/test/regression/regression.cc b/test/regression/regression.cc index 5b1ca514e..2f2da5ebf 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 000000000..08b2793b3 --- /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 ebda49fb8..ffc4e486e 100644 --- a/test/test-suite.in +++ b/test/test-suite.in @@ -88,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