From 58fc3dac3d42aff89e6ce80ba1b0080e36ca730f Mon Sep 17 00:00:00 2001 From: jens Date: Sun, 29 Mar 2026 11:31:51 +0200 Subject: [PATCH 1/6] Update libinjection to v4.0.0 --- others/libinjection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/others/libinjection b/others/libinjection index b9fcaaf9e..211782219 160000 --- a/others/libinjection +++ b/others/libinjection @@ -1 +1 @@ -Subproject commit b9fcaaf9e50e9492807b23ffcc6af46ee1f203b9 +Subproject commit 211782219663f889f471650150df12b623c5766e From e1a527ea6ec2c839344a20361724bd3e762391bd Mon Sep 17 00:00:00 2001 From: jens Date: Sun, 29 Mar 2026 11:39:48 +0200 Subject: [PATCH 2/6] Make detect_sqli and detect_xss compatible with libinjection v4 --- src/operators/detect_sqli.cc | 79 ++++++++++++++++++++---------- src/operators/detect_xss.cc | 60 ++++++++++++++--------- src/operators/libinjection_utils.h | 48 ++++++++++++++++++ 3 files changed, 137 insertions(+), 50 deletions(-) create mode 100644 src/operators/libinjection_utils.h diff --git a/src/operators/detect_sqli.cc b/src/operators/detect_sqli.cc index 49cef935c..05cefc543 100644 --- a/src/operators/detect_sqli.cc +++ b/src/operators/detect_sqli.cc @@ -17,45 +17,70 @@ #include #include +#include #include "src/operators/operator.h" +#include "src/operators/libinjection_utils.h" #include "libinjection/src/libinjection.h" +#include "libinjection/src/libinjection_error.h" -namespace modsecurity { -namespace operators { - +namespace modsecurity::operators { bool DetectSQLi::evaluate(Transaction *t, RuleWithActions *rule, const std::string& input, RuleMessage &ruleMessage) { - char fingerprint[8]; - int issqli; - issqli = libinjection_sqli(input.c_str(), input.length(), fingerprint); + std::array fingerprint{}; + + const injection_result_t sqli_result = + libinjection_sqli(input.c_str(), input.length(), fingerprint.data()); - if (!t) { - goto tisempty; + if (t == nullptr) { + return isMaliciousLibinjectionResult(sqli_result); } - if (issqli) { - t->m_matched.push_back(fingerprint); - ms_dbg_a(t, 4, "detected SQLi using libinjection with " \ - "fingerprint '" + std::string(fingerprint) + "' at: '" + - input + "'"); - if (rule && rule->hasCaptureAction()) { - t->m_collections.m_tx_collection->storeOrUpdateFirst( - "0", std::string(fingerprint)); - ms_dbg_a(t, 7, "Added DetectSQLi match TX.0: " + \ - std::string(fingerprint)); - } - } else { - ms_dbg_a(t, 9, "detected SQLi: not able to find an " \ - "inject on '" + input + "'"); + switch (sqli_result) { + case LIBINJECTION_RESULT_TRUE: + t->m_matched.emplace_back(fingerprint.data()); + + ms_dbg_a(t, 4, + std::string("detected SQLi using libinjection with fingerprint '") + + fingerprint.data() + "' at: '" + input + "'") + + if (rule != nullptr && rule->hasCaptureAction()) { + t->m_collections.m_tx_collection->storeOrUpdateFirst( + "0", std::string(fingerprint.data())); + + ms_dbg_a(t, 7, + std::string("Added DetectSQLi match TX.0: ") + + fingerprint.data()) + } + break; + + case LIBINJECTION_RESULT_ERROR: + ms_dbg_a(t, 4, + std::string("libinjection parser error during SQLi analysis (") + + libinjectionResultToString(sqli_result) + + "); treating as match (fail-safe). Input: '" + + input + "'") + + if (rule != nullptr && rule->hasCaptureAction()) { + t->m_collections.m_tx_collection->storeOrUpdateFirst( + "0", input); + + ms_dbg_a(t, 7, + std::string("Added DetectSQLi error input TX.0: ") + + input) + } + break; + + case LIBINJECTION_RESULT_FALSE: + ms_dbg_a(t, 9, + std::string("libinjection was not able to find any SQLi in: ") + + input) + break; } -tisempty: - return issqli != 0; + return isMaliciousLibinjectionResult(sqli_result); } - -} // namespace operators -} // namespace modsecurity +} // namespace modsecurity::operators diff --git a/src/operators/detect_xss.cc b/src/operators/detect_xss.cc index 014202e73..9782de537 100644 --- a/src/operators/detect_xss.cc +++ b/src/operators/detect_xss.cc @@ -18,36 +18,50 @@ #include #include "src/operators/operator.h" +#include "src/operators/libinjection_utils.h" #include "libinjection/src/libinjection.h" +#include "libinjection/src/libinjection_error.h" - -namespace modsecurity { -namespace operators { - +namespace modsecurity::operators { bool DetectXSS::evaluate(Transaction *t, RuleWithActions *rule, const std::string& input, RuleMessage &ruleMessage) { - int is_xss; - - is_xss = libinjection_xss(input.c_str(), input.length()); - - if (t) { - if (is_xss) { - ms_dbg_a(t, 5, "detected XSS using libinjection."); - if (rule && rule->hasCaptureAction()) { - t->m_collections.m_tx_collection->storeOrUpdateFirst( - "0", std::string(input)); - ms_dbg_a(t, 7, "Added DetectXSS match TX.0: " + \ - std::string(input)); + + const injection_result_t xss_result = + libinjection_xss(input.c_str(), input.length()); + + if (t == nullptr) { + return isMaliciousLibinjectionResult(xss_result); + } + + switch (xss_result) { + case LIBINJECTION_RESULT_TRUE: + ms_dbg_a(t, 5, std::string("detected XSS using libinjection.")) + if (rule != nullptr && rule->hasCaptureAction()) { + t->m_collections.m_tx_collection->storeOrUpdateFirst("0", input); + ms_dbg_a(t, 7, std::string("Added DetectXSS match TX.0: ") + input) } - } else { - ms_dbg_a(t, 9, "libinjection was not able to " \ - "find any XSS in: " + input); + break; + + case LIBINJECTION_RESULT_ERROR: + ms_dbg_a(t, 4, + std::string("libinjection parser error during XSS analysis (") + + libinjectionResultToString(xss_result) + + "); treating as match (fail-safe). Input: " + + input) + if (rule != nullptr && rule->hasCaptureAction()) { + t->m_collections.m_tx_collection->storeOrUpdateFirst("0", input); + ms_dbg_a(t, 7, std::string("Added DetectXSS error input TX.0: ") + input) } + break; + + case LIBINJECTION_RESULT_FALSE: + ms_dbg_a(t, 9, + std::string("libinjection was not able to find any XSS in: ") + input) + break; } - return is_xss != 0; -} + return isMaliciousLibinjectionResult(xss_result); +} -} // namespace operators -} // namespace modsecurity +} // namespace modsecurity::operators diff --git a/src/operators/libinjection_utils.h b/src/operators/libinjection_utils.h new file mode 100644 index 000000000..3c543d038 --- /dev/null +++ b/src/operators/libinjection_utils.h @@ -0,0 +1,48 @@ +/* + * 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. + * + */ + +#ifndef SRC_OPERATORS_LIBINJECTION_UTILS_H_ +#define SRC_OPERATORS_LIBINJECTION_UTILS_H_ + +#include "libinjection/src/libinjection_error.h" + +namespace modsecurity::operators { + +/* + * libinjection parser errors are handled in fail-safe mode as suspicious + * results, so callers can block on both confirmed detections and parser + * failures. + */ +static inline bool isMaliciousLibinjectionResult(injection_result_t result) { + return result == LIBINJECTION_RESULT_TRUE + || result == LIBINJECTION_RESULT_ERROR; +} + +static inline const char *libinjectionResultToString(injection_result_t result) { + switch (result) { + case LIBINJECTION_RESULT_TRUE: + return "attack-detected"; + case LIBINJECTION_RESULT_FALSE: + return "no-attack"; + case LIBINJECTION_RESULT_ERROR: + return "parser-error"; + } + + return "unexpected-result"; +} + +} // namespace modsecurity::operators + +#endif // SRC_OPERATORS_LIBINJECTION_UTILS_H_ From d2cef415b84e667a38d69e8ccecd87ef973b4fe6 Mon Sep 17 00:00:00 2001 From: jens Date: Sun, 29 Mar 2026 11:44:08 +0200 Subject: [PATCH 3/6] Update regression tests for libinjection v4 compatibility --- .../regression/operator-detectsqli.json | 330 +++++++++++++++++- .../regression/operator-detectxss.json | 286 ++++++++++++++- 2 files changed, 608 insertions(+), 8 deletions(-) diff --git a/test/test-cases/regression/operator-detectsqli.json b/test/test-cases/regression/operator-detectsqli.json index 657bdf388..fc49b0754 100644 --- a/test/test-cases/regression/operator-detectsqli.json +++ b/test/test-cases/regression/operator-detectsqli.json @@ -2,7 +2,6 @@ { "enabled": 1, "version_min": 300000, - "title": "Testing Operator :: @detectSQLi", "client": { "ip": "200.249.12.31", "port": 123 @@ -11,6 +10,18 @@ "ip": "200.249.12.31", "port": 80 }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: known stable fingerprint ascii substring", "request": { "headers": { "Host": "localhost", @@ -25,6 +36,212 @@ "param1=ascii(substring(version() from 1 for 1))¶m2=value2" ] }, + "expected": { + "http_code": 403, + "debug_log": "Added DetectSQLi match TX.0: f\\(f\\(f" + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1201,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2201,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: trim still detects stable fingerprint", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "67", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1= ascii(substring(version() from 1 for 1)) ¶m2=value2" + ] + }, + "expected": { + "http_code": 403, + "debug_log": "Added DetectSQLi match TX.0: f\\(f\\(f" + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1202,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2202,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: boolean based payload", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "33", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=' or 1=1 -- ¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1203,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2203,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: union select variation", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "46", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=-1 union select 1,2,3 -- ¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1204,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2204,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: time function payload", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "38", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=1;select sleep(1)¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1205,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2205,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, "response": { "headers": { "Date": "Mon, 13 Jul 2015 20:02:41 GMT", @@ -36,13 +253,120 @@ "no need." ] }, + "title": "Testing Operator :: @detectSQLi :: inline comment obfuscation", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "35", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=1/**/or/**/1=1¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1206,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2206,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: benign identifier with sql words", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "38", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=selective_catalog¶m2=normal" + ] + }, + "expected": { + "http_code": 200 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectSQLi\" \"id:1207,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2207,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectSQLi :: numeric edge case should not match", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "23", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=100001¶m2=42" + ] + }, "expected": { - "debug_log": "Added DetectSQLi match TX.0: f\\(f\\(f", "http_code": 200 }, "rules": [ "SecRuleEngine On", - "SecRule ARGS \"@detectSQLi\" \"id:1,phase:2,capture,pass,t:trim\"" + "SecRule ARGS \"@detectSQLi\" \"id:1208,phase:2,capture,pass,t:trim,setvar:tx.sqli_hit=1\"", + "SecRule TX:sqli_hit \"@eq 1\" \"id:2208,phase:2,deny,status:403\"" ] } ] diff --git a/test/test-cases/regression/operator-detectxss.json b/test/test-cases/regression/operator-detectxss.json index 9e6fa24b2..8cc651f6d 100644 --- a/test/test-cases/regression/operator-detectxss.json +++ b/test/test-cases/regression/operator-detectxss.json @@ -2,7 +2,6 @@ { "enabled": 1, "version_min": 300000, - "title": "Testing Operator :: @detectXSS", "client": { "ip": "200.249.12.31", "port": 123 @@ -11,20 +10,144 @@ "ip": "200.249.12.31", "port": 80 }, + "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." + ] + }, + "title": "Testing Operator :: @detectXSS :: script tag payload", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "46", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectXSS\" \"id:1101,phase:2,pass,t:trim,setvar:tx.xss_hit=1\"", + "SecRule TX:xss_hit \"@eq 1\" \"id:2101,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectXSS :: trim preserves detection", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "52", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1= ¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectXSS\" \"id:1102,phase:2,pass,t:trim,setvar:tx.xss_hit=1\"", + "SecRule TX:xss_hit \"@eq 1\" \"id:2102,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectXSS :: img onerror payload", "request": { "headers": { "Host": "localhost", "User-Agent": "curl/7.38.0", "Accept": "*/*", - "Content-Length": "45", + "Content-Length": "49", "Content-Type": "application/x-www-form-urlencoded" }, "uri": "/", "method": "POST", "body": [ - "param1=¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectXSS\" \"id:1108,phase:2,capture,pass,t:trim,setvar:tx.xss_hit=1\"", + "SecRule TX:0 \"@streq \" \"id:2108,phase:2,deny,status:403\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "client": { + "ip": "200.249.12.31", + "port": 123 + }, + "server": { + "ip": "200.249.12.31", + "port": 80 + }, + "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." + ] + }, + "title": "Testing Operator :: @detectXSS :: no capture keeps TX.0 unchanged", + "request": { + "headers": { + "Host": "localhost", + "User-Agent": "curl/7.38.0", + "Accept": "*/*", + "Content-Length": "46", + "Content-Type": "application/x-www-form-urlencoded" + }, + "uri": "/", + "method": "POST", + "body": [ + "param1=¶m2=value2" + ] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecRule ARGS \"@detectXSS\" \"id:1109,phase:2,pass,t:trim,setvar:tx.xss_hit=1\"", + "SecRule TX:0 \"@streq \" \"id:2109,phase:2,deny,status:409\"", + "SecRule TX:xss_hit \"@eq 1\" \"id:2110,phase:2,deny,status:403\"" + ] } ] From 7cd1d6718982b20abe81fb3eda220cced2763dc3 Mon Sep 17 00:00:00 2001 From: Easton97-Jens <66330090+Easton97-Jens@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:22:13 +0200 Subject: [PATCH 6/6] Fix Windows test include path and case-insensitive override matching --- build/win32/CMakeLists.txt | 4 +- src/Makefile.am | 1 + src/operators/detect_sqli.cc | 7 +- src/operators/detect_xss.cc | 4 +- src/operators/libinjection_adapter.cc | 46 +++++++++++++ src/operators/libinjection_adapter.h | 27 ++++++++ test/Makefile.am | 1 + .../unit/operator-libinjection-error.json | 40 ++++++++++++ test/test-suite.in | 1 + test/unit/unit.cc | 65 ++++++++++++++++++- test/unit/unit_test.cc | 8 ++- test/unit/unit_test.h | 2 + 12 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 src/operators/libinjection_adapter.cc create mode 100644 src/operators/libinjection_adapter.h create mode 100644 test/test-cases/unit/operator-libinjection-error.json diff --git a/build/win32/CMakeLists.txt b/build/win32/CMakeLists.txt index fbf39f08d..5ee49714d 100644 --- a/build/win32/CMakeLists.txt +++ b/build/win32/CMakeLists.txt @@ -164,7 +164,7 @@ project(libModSecurityTests) function(setTestTargetProperties executable) target_compile_definitions(${executable} PRIVATE WITH_PCRE2) - target_include_directories(${executable} PRIVATE ${BASE_DIR} ${BASE_DIR}/headers) + target_include_directories(${executable} PRIVATE ${BASE_DIR} ${BASE_DIR}/headers ${BASE_DIR}/others) target_link_libraries(${executable} PRIVATE libModSecurity pcre2::pcre2 dirent::dirent) add_package_dependency(${executable} WITH_YAJL yajl::yajl HAVE_YAJL) endfunction() @@ -239,7 +239,7 @@ setTestTargetProperties(rules_optimization) project(libModSecurityExamples) function(setExampleTargetProperties executable) - target_include_directories(${executable} PRIVATE ${BASE_DIR} ${BASE_DIR}/headers) + target_include_directories(${executable} PRIVATE ${BASE_DIR} ${BASE_DIR}/headers ${BASE_DIR}/others) target_link_libraries(${executable} PRIVATE libModSecurity) endfunction() diff --git a/src/Makefile.am b/src/Makefile.am index 14c26697b..476585eda 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -187,6 +187,7 @@ OPERATORS = \ operators/contains_word.cc \ operators/detect_sqli.cc \ operators/detect_xss.cc \ + operators/libinjection_adapter.cc \ operators/ends_with.cc \ operators/eq.cc \ operators/fuzzy_hash.cc \ diff --git a/src/operators/detect_sqli.cc b/src/operators/detect_sqli.cc index 5e3014d29..868090937 100644 --- a/src/operators/detect_sqli.cc +++ b/src/operators/detect_sqli.cc @@ -21,7 +21,7 @@ #include "src/operators/operator.h" #include "src/operators/libinjection_utils.h" -#include "libinjection/src/libinjection.h" +#include "src/operators/libinjection_adapter.h" #include "libinjection/src/libinjection_error.h" namespace modsecurity::operators { @@ -32,7 +32,7 @@ bool DetectSQLi::evaluate(Transaction *t, RuleWithActions *rule, std::array fingerprint{}; const injection_result_t sqli_result = - libinjection_sqli(input.c_str(), input.length(), fingerprint.data()); + runLibinjectionSQLi(input.c_str(), input.length(), fingerprint.data()); if (t == nullptr) { return isMaliciousLibinjectionResult(sqli_result); @@ -71,6 +71,9 @@ bool DetectSQLi::evaluate(Transaction *t, RuleWithActions *rule, std::string("Added DetectSQLi error input TX.0: ") + input); } + + // Keep m_matched untouched for parser-error paths to avoid + // introducing synthetic fingerprints for non-TRUE results. break; case LIBINJECTION_RESULT_FALSE: diff --git a/src/operators/detect_xss.cc b/src/operators/detect_xss.cc index ff6d36782..e9ecf00a2 100644 --- a/src/operators/detect_xss.cc +++ b/src/operators/detect_xss.cc @@ -19,7 +19,7 @@ #include "src/operators/operator.h" #include "src/operators/libinjection_utils.h" -#include "libinjection/src/libinjection.h" +#include "src/operators/libinjection_adapter.h" #include "libinjection/src/libinjection_error.h" namespace modsecurity::operators { @@ -28,7 +28,7 @@ bool DetectXSS::evaluate(Transaction *t, RuleWithActions *rule, const std::string& input, RuleMessage &ruleMessage) { const injection_result_t xss_result = - libinjection_xss(input.c_str(), input.length()); + runLibinjectionXSS(input.c_str(), input.length()); if (t == nullptr) { return isMaliciousLibinjectionResult(xss_result); diff --git a/src/operators/libinjection_adapter.cc b/src/operators/libinjection_adapter.cc new file mode 100644 index 000000000..f2a50163f --- /dev/null +++ b/src/operators/libinjection_adapter.cc @@ -0,0 +1,46 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + */ + +#include "src/operators/libinjection_adapter.h" + +#include "libinjection/src/libinjection.h" + +namespace modsecurity::operators { +namespace { +// Per-thread overrides avoid cross-thread interference during mtstress tests. +thread_local DetectSQLiFn g_sqli_override = nullptr; +thread_local DetectXSSFn g_xss_override = nullptr; +} + +injection_result_t runLibinjectionSQLi(const char *input, size_t len, + char *fingerprint) { + if (DetectSQLiFn fn = g_sqli_override) { + return fn(input, len, fingerprint); + } + + return libinjection_sqli(input, len, fingerprint); +} + +injection_result_t runLibinjectionXSS(const char *input, size_t len) { + if (DetectXSSFn fn = g_xss_override) { + return fn(input, len); + } + + return libinjection_xss(input, len); +} + +void setLibinjectionSQLiOverrideForTesting(DetectSQLiFn fn) { + g_sqli_override = fn; +} + +void setLibinjectionXSSOverrideForTesting(DetectXSSFn fn) { + g_xss_override = fn; +} + +void clearLibinjectionOverridesForTesting() { + g_sqli_override = nullptr; + g_xss_override = nullptr; +} + +} // namespace modsecurity::operators diff --git a/src/operators/libinjection_adapter.h b/src/operators/libinjection_adapter.h new file mode 100644 index 000000000..e8ed04f01 --- /dev/null +++ b/src/operators/libinjection_adapter.h @@ -0,0 +1,27 @@ +/* + * ModSecurity, http://www.modsecurity.org/ + */ + +#ifndef SRC_OPERATORS_LIBINJECTION_ADAPTER_H_ +#define SRC_OPERATORS_LIBINJECTION_ADAPTER_H_ + +#include + +#include "libinjection/src/libinjection_error.h" // matches detect_xss.cc, detect_sqli.cc, and libinjection_utils.h + +namespace modsecurity::operators { + +using DetectSQLiFn = injection_result_t (*)(const char *, size_t, char *); +using DetectXSSFn = injection_result_t (*)(const char *, size_t); + +injection_result_t runLibinjectionSQLi(const char *input, size_t len, + char *fingerprint); +injection_result_t runLibinjectionXSS(const char *input, size_t len); + +void setLibinjectionSQLiOverrideForTesting(DetectSQLiFn fn); +void setLibinjectionXSSOverrideForTesting(DetectXSSFn fn); +void clearLibinjectionOverridesForTesting(); + +} // namespace modsecurity::operators + +#endif // SRC_OPERATORS_LIBINJECTION_ADAPTER_H_ diff --git a/test/Makefile.am b/test/Makefile.am index 2e7e05d61..de8d4f4af 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -73,6 +73,7 @@ unit_tests_LDFLAGS = \ unit_tests_CPPFLAGS = \ -Icommon \ -I$(top_srcdir)/ \ + -I$(top_srcdir)/others \ -g \ -I$(top_builddir)/headers \ $(CURL_CFLAGS) \ diff --git a/test/test-cases/unit/operator-libinjection-error.json b/test/test-cases/unit/operator-libinjection-error.json new file mode 100644 index 000000000..ef440cd4c --- /dev/null +++ b/test/test-cases/unit/operator-libinjection-error.json @@ -0,0 +1,40 @@ +[ + { + "type": "op", + "name": "detectXSS", + "param": "", + "input": "", + "ret": 1, + "capture": 1, + "libinjection_override": "error", + "output": "" + }, + { + "type": "op", + "name": "detectSQLi", + "param": "", + "input": "''''''", + "ret": 1, + "capture": 1, + "libinjection_override": "error", + "output": "''''''" + }, + { + "type": "op", + "name": "detectXSS", + "param": "", + "input": "", + "ret": 1, + "capture": 1, + "output": "" + }, + { + "type": "op", + "name": "detectSQLi", + "param": "", + "input": "ascii(substring(version() from 1 for 1))", + "ret": 1, + "capture": 1, + "output": "f(f(f" + } +] diff --git a/test/test-suite.in b/test/test-suite.in index 6e8754254..c667bf677 100644 --- a/test/test-suite.in +++ b/test/test-suite.in @@ -198,6 +198,7 @@ TESTS+=test/test-cases/secrules-language-tests/operators/contains.json TESTS+=test/test-cases/secrules-language-tests/operators/containsWord.json TESTS+=test/test-cases/secrules-language-tests/operators/detectSQLi.json TESTS+=test/test-cases/secrules-language-tests/operators/detectXSS.json +TESTS+=test/test-cases/unit/operator-libinjection-error.json TESTS+=test/test-cases/secrules-language-tests/operators/endsWith.json TESTS+=test/test-cases/secrules-language-tests/operators/eq.json TESTS+=test/test-cases/secrules-language-tests/operators/ge.json diff --git a/test/unit/unit.cc b/test/unit/unit.cc index 8bf5954d2..0cb20f2f6 100644 --- a/test/unit/unit.cc +++ b/test/unit/unit.cc @@ -28,6 +28,8 @@ #include "src/actions/transformations/transformation.h" #include "modsecurity/transaction.h" #include "modsecurity/actions/action.h" +#include "src/actions/capture.h" +#include "src/operators/libinjection_adapter.h" #include "test/common/modsecurity_test.h" @@ -57,6 +59,34 @@ void print_help() { } +namespace { +injection_result_t sqli_force_error(const char *, size_t, char *) { + return LIBINJECTION_RESULT_ERROR; +} + +injection_result_t xss_force_error(const char *, size_t) { + return LIBINJECTION_RESULT_ERROR; +} + +void configure_libinjection_override(const UnitTest &t) { + modsecurity::operators::clearLibinjectionOverridesForTesting(); + + if (t.libinjection_override != "error") { + return; + } + + const std::string operator_name = modsecurity::utils::string::tolower(t.name); + + if (operator_name == "detectsqli") { + modsecurity::operators::setLibinjectionSQLiOverrideForTesting( + sqli_force_error); + } else if (operator_name == "detectxss") { + modsecurity::operators::setLibinjectionXSSOverrideForTesting( + xss_force_error); + } +} +} // namespace + struct OperatorTest { using ItemType = Operator; @@ -71,13 +101,42 @@ struct OperatorTest { } static UnitTestResult eval(ItemType &op, const UnitTest &t, modsecurity::Transaction &transaction) { - modsecurity::RuleWithActions rule{nullptr, nullptr, "dummy.conf", -1}; + configure_libinjection_override(t); + + std::unique_ptr actions; + if (t.capture) { + actions = std::make_unique(); + actions->push_back(new modsecurity::actions::Capture("capture")); + } + + modsecurity::RuleWithActions rule{actions.release(), nullptr, "dummy.conf", -1}; modsecurity::RuleMessage ruleMessage{rule, transaction}; - return {op.evaluate(&transaction, &rule, t.input, ruleMessage), {}}; + + const bool matched = op.evaluate(&transaction, &rule, t.input, ruleMessage); + + UnitTestResult result; + result.ret = matched; + if (t.capture) { + auto tx0 = transaction.m_collections.m_tx_collection->resolveFirst("0"); + if (tx0 != nullptr) { + result.output = *tx0; + } + } + + modsecurity::operators::clearLibinjectionOverridesForTesting(); + return result; } static bool check(const UnitTestResult &result, const UnitTest &t) { - return result.ret != t.ret; + if (result.ret != t.ret) { + return true; + } + + if (t.capture || t.output.empty() == false) { + return result.output != t.output; + } + + return false; } }; diff --git a/test/unit/unit_test.cc b/test/unit/unit_test.cc index e67c10052..36e1c5a3a 100644 --- a/test/unit/unit_test.cc +++ b/test/unit/unit_test.cc @@ -110,11 +110,13 @@ std::unique_ptr UnitTest::from_yajl_node(const yajl_val &node) { size_t num_tests = node->u.object.len; auto u = std::make_unique(); + u->skipped = false; + u->capture = 0; + for (int i = 0; i < num_tests; i++) { const char *key = node->u.object.keys[ i ]; yajl_val val = node->u.object.values[ i ]; - u->skipped = false; if (strcmp(key, "param") == 0) { u->param = YAJL_GET_STRING(val); } else if (strcmp(key, "input") == 0) { @@ -128,6 +130,10 @@ std::unique_ptr UnitTest::from_yajl_node(const yajl_val &node) { u->type = YAJL_GET_STRING(val); } else if (strcmp(key, "ret") == 0) { u->ret = YAJL_GET_INTEGER(val); + } else if (strcmp(key, "capture") == 0) { + u->capture = YAJL_GET_INTEGER(val); + } else if (strcmp(key, "libinjection_override") == 0) { + u->libinjection_override = YAJL_GET_STRING(val); } else if (strcmp(key, "output") == 0) { u->output = std::string(YAJL_GET_STRING(val)); json2bin(&u->output); diff --git a/test/unit/unit_test.h b/test/unit/unit_test.h index ffd776442..95257d706 100644 --- a/test/unit/unit_test.h +++ b/test/unit/unit_test.h @@ -44,7 +44,9 @@ class UnitTest { std::string type; std::string filename; std::string output; + std::string libinjection_override; int ret; + int capture; int skipped; UnitTestResult result; };