diff --git a/src/collection/backend/in_memory-per_process.cc b/src/collection/backend/in_memory-per_process.cc index b16ee843ac..bdf3d09e68 100644 --- a/src/collection/backend/in_memory-per_process.cc +++ b/src/collection/backend/in_memory-per_process.cc @@ -194,14 +194,22 @@ void InMemoryPerProcess::resolveMultiMatches(const std::string& var, void InMemoryPerProcess::resolveRegularExpression(const std::string& var, std::vector *l, variables::KeyExclusions &ke) { + // Callers that do not hold a compiled regex (e.g. the compartment-prefixed + // overloads) still pay the compilation cost here. Utils::Regex r(var, true); + resolveRegularExpression(&r, l, ke); +} + + +void InMemoryPerProcess::resolveRegularExpression(const Utils::Regex *r, + std::vector *l, variables::KeyExclusions &ke) { std::list expiredVars; { const std::shared_lock lock(m_mutex); // read lock (shared access) for (const auto& x : m_map) { - const auto ret = Utils::regex_search(x.first, r); + const auto ret = Utils::regex_search(x.first, *r); if (ret <= 0) { continue; } diff --git a/src/collection/backend/in_memory-per_process.h b/src/collection/backend/in_memory-per_process.h index 4aa7b1d076..9f7d9506f4 100644 --- a/src/collection/backend/in_memory-per_process.h +++ b/src/collection/backend/in_memory-per_process.h @@ -99,6 +99,13 @@ class InMemoryPerProcess : void resolveRegularExpression(const std::string& var, std::vector *l, variables::KeyExclusions &ke) override; + // Concrete fast path: reuse an already-compiled regex (e.g. the one a + // VariableRegex holds in m_r) instead of recompiling the pattern on every + // call. Intentionally NOT declared on the Collection base class, to keep + // that public interface's vtable - and therefore its ABI - unchanged. + void resolveRegularExpression(const Utils::Regex *r, + std::vector *l, + variables::KeyExclusions &ke); /* store */ virtual void store(const std::string &key, std::string &compartment, diff --git a/src/variables/tx.h b/src/variables/tx.h index 1fae827f13..59e2103c8e 100644 --- a/src/variables/tx.h +++ b/src/variables/tx.h @@ -25,6 +25,7 @@ #include "src/variables/variable.h" #include "src/run_time_string.h" +#include "src/collection/backend/in_memory-per_process.h" namespace modsecurity { @@ -66,17 +67,26 @@ class Tx_NoDictElement : public Variable { class Tx_DictElementRegexp : public VariableRegex { public: explicit Tx_DictElementRegexp(const std::string &dictElement) - : VariableRegex("TX", dictElement), - m_dictElement(dictElement) { } + : VariableRegex("TX", dictElement) { } void evaluate(Transaction *t, RuleWithActions *rule, std::vector *l) override { - t->m_collections.m_tx_collection->resolveRegularExpression( - m_dictElement, l, m_keyExclusion); + // TX is always backed by InMemoryPerProcess. Reuse the regex compiled + // once in VariableRegex::m_r via the backend's concrete fast path, + // instead of recompiling the pattern on every transaction. This is kept + // off the Collection base class to avoid an ABI-breaking vtable change; + // any other backend falls back to the string-based resolution. + auto *collection = t->m_collections.m_tx_collection; + if (auto *inMemory = + dynamic_cast( + collection)) { + inMemory->resolveRegularExpression(&m_r, l, m_keyExclusion); + } else { + collection->resolveRegularExpression(m_r.pattern, l, + m_keyExclusion); + } } - - std::string m_dictElement; }; diff --git a/test/test-cases/regression/variable-tx-regex-precompiled.json b/test/test-cases/regression/variable-tx-regex-precompiled.json new file mode 100644 index 0000000000..870cb2170c --- /dev/null +++ b/test/test-cases/regression/variable-tx-regex-precompiled.json @@ -0,0 +1,78 @@ +[ + { + "enabled": 1, + "version_min": 300000, + "title": "TX regex variable selector (precompiled regex) - matches in a chained rule", + "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", + "Content-Length": "0" + }, + "uri": "/test", + "method": "GET" + }, + "response": { + "headers": { + "Content-Type": "text/html", + "Content-Length": "8" + }, + "body": ["no need."] + }, + "expected": { + "http_code": 403 + }, + "rules": [ + "SecRuleEngine On", + "SecAction \"id:1,phase:1,nolog,pass,setvar:'tx.score_a=1',setvar:'tx.score_b=1',setvar:'tx.other=1'\"", + "SecRule REQUEST_URI \"@contains /test\" \"id:2,phase:2,deny,status:403,log,chain\"", + " SecRule TX:/^score_/ \"@eq 1\" \"t:none\"" + ] + }, + { + "enabled": 1, + "version_min": 300000, + "title": "TX regex variable selector (precompiled regex) - selector excludes non-matching keys", + "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", + "Content-Length": "0" + }, + "uri": "/test", + "method": "GET" + }, + "response": { + "headers": { + "Content-Type": "text/html", + "Content-Length": "8" + }, + "body": ["no need."] + }, + "expected": { + "http_code": 200 + }, + "rules": [ + "SecRuleEngine On", + "SecAction \"id:1,phase:1,nolog,pass,setvar:'tx.other=1'\"", + "SecRule REQUEST_URI \"@contains /test\" \"id:2,phase:2,deny,status:403,log,chain\"", + " SecRule TX:/^score_/ \"@eq 1\" \"t:none\"" + ] + } +]