Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isolated PCRE match limits as a layer of ReDoS defense #2736

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions headers/modsecurity/rules_set_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ class RulesSetProperties {
from->m_responseBodyLimitAction,
PropertyNotSetBodyLimitAction);

to->m_pcreMatchLimit.merge(&from->m_pcreMatchLimit);
to->m_uploadFileLimit.merge(&from->m_uploadFileLimit);
to->m_uploadFileMode.merge(&from->m_uploadFileMode);
to->m_uploadDirectory.merge(&from->m_uploadDirectory);
Expand Down Expand Up @@ -470,6 +471,7 @@ class RulesSetProperties {
ConfigDouble m_requestBodyLimit;
ConfigDouble m_requestBodyNoFilesLimit;
ConfigDouble m_responseBodyLimit;
ConfigInt m_pcreMatchLimit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with this size. 'int' should be big enough on any architecture that matters.

Note, however, the disparity between pcre1 and pcre2 (long vs. uint32_t) for this. This may another reason to avoid doing the get_default

ConfigInt m_uploadFileLimit;
ConfigInt m_uploadFileMode;
DebugLog *m_debugLog;
Expand Down
4 changes: 4 additions & 0 deletions headers/modsecurity/transaction.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ class TransactionAnchoredVariables {
m_variableInboundDataError(t, "INBOUND_DATA_ERROR"),
m_variableMatchedVar(t, "MATCHED_VAR"),
m_variableMatchedVarName(t, "MATCHED_VAR_NAME"),
m_variableMscPcreError(t, "MSC_PCRE_ERROR"),
m_variableMscPcreLimitsExceeded(t, "MSC_PCRE_LIMITS_EXCEEDED"),
m_variableMultipartBoundaryQuoted(t, "MULTIPART_BOUNDARY_QUOTED"),
m_variableMultipartBoundaryWhiteSpace(t,
"MULTIPART_BOUNDARY_WHITESPACE"),
Expand Down Expand Up @@ -219,6 +221,8 @@ class TransactionAnchoredVariables {
AnchoredVariable m_variableInboundDataError;
AnchoredVariable m_variableMatchedVar;
AnchoredVariable m_variableMatchedVarName;
AnchoredVariable m_variableMscPcreError;
AnchoredVariable m_variableMscPcreLimitsExceeded;
AnchoredVariable m_variableMultipartBoundaryQuoted;
AnchoredVariable m_variableMultipartBoundaryWhiteSpace;
AnchoredVariable m_variableMultipartCrlfLFLines;
Expand Down
28 changes: 26 additions & 2 deletions src/operators/rx.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,36 @@ bool Rx::evaluate(Transaction *transaction, RuleWithActions *rule,
re = m_re;
}

std::vector<Utils::SMatchCapture> captures;
if (re->hasError()) {
ms_dbg_a(transaction, 3, "Error with regular expression: \"" + re->pattern + "\"");
return false;
}
re->searchOneMatch(input, captures);

Utils::RegexResult regex_result;
std::vector<Utils::SMatchCapture> captures;

if (transaction && transaction->m_rules->m_pcreMatchLimit.m_set) {
unsigned long match_limit = transaction->m_rules->m_pcreMatchLimit.m_value;
regex_result = re->searchOneMatch(input, captures, match_limit);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we abandon the get_default functionality, I suspect there wouldn't be a compelling reason to overload this function (or the 'Global' equivalent). A single function could either have the second argument explicitly passed something like 0 or -1 (or the second arg could be declared with a default of one of those).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and added a default value for the match_limit param in the declarations in src/utils/regex.h and left this logic the same. My thinking is that, this way, only regex.cc needs to worry about the default value.

} else {
regex_result = re->searchOneMatch(input, captures);
}

// FIXME: DRY regex error reporting. This logic is currently duplicated in other operators.
if (regex_result != Utils::RegexResult::Ok) {
transaction->m_variableMscPcreError.set("1", transaction->m_variableOffset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error variable gets set to '1' when an error is encountered, but there is no setting to '0' otherwise -- it's just not set to anything.

ModSecurity error variables should have testable value for 'no error' and this is conventionally '0'. Often this the variable is set to either 0 or 1 upon completion of the single task per transaction (such as REQBODY_ERROR). You won't be able to do that here. The best analog is URL_ENCODED_ERROR, which is initialized to 0 in the Transaction constructors.

Same thing with m_variableMscPcreLimitsExceeded

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. It's definitely good to explicitly set it to a sensible initial value. Done.


std::string regex_error_str = "OTHER";
if (regex_result == Utils::RegexResult::ErrorMatchLimit) {
regex_error_str = "MATCH_LIMIT";
transaction->m_variableMscPcreLimitsExceeded.set("1", transaction->m_variableOffset);
}

ms_dbg_a(transaction, 1, "rx: regex error '" + regex_error_str + "' for pattern '" + re->pattern + "'");


return false;
}

if (rule && rule->hasCaptureAction() && transaction) {
for (const Utils::SMatchCapture& capture : captures) {
Expand Down
24 changes: 23 additions & 1 deletion src/operators/rx_global.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,30 @@ bool RxGlobal::evaluate(Transaction *transaction, RuleWithActions *rule,
re = m_re;
}

Utils::RegexResult regex_result;
std::vector<Utils::SMatchCapture> captures;
re->searchGlobal(input, captures);
if (transaction && transaction->m_rules->m_pcreMatchLimit.m_set) {
unsigned long match_limit = transaction->m_rules->m_pcreMatchLimit.m_value;
regex_result = re->searchGlobal(input, captures, match_limit);
} else {
regex_result = re->searchGlobal(input, captures);
}

// FIXME: DRY regex error reporting. This logic is currently duplicated in other operators.
if (regex_result != Utils::RegexResult::Ok) {
transaction->m_variableMscPcreError.set("1", transaction->m_variableOffset);

std::string regex_error_str = "OTHER";
if (regex_result == Utils::RegexResult::ErrorMatchLimit) {
regex_error_str = "MATCH_LIMIT";
transaction->m_variableMscPcreLimitsExceeded.set("1", transaction->m_variableOffset);
}

ms_dbg_a(transaction, 1, "rxGlobal: regex error '" + regex_error_str + "' for pattern '" + re->pattern + "'");

return false;
}


if (rule && rule->hasCaptureAction() && transaction) {
for (const Utils::SMatchCapture& capture : captures) {
Expand Down
2 changes: 1 addition & 1 deletion src/parser/location.hh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// A Bison parser, made by GNU Bison 3.7.6.
// A Bison parser, made by GNU Bison 3.8.2.

// Locations for Bison parsers in C++

Expand Down
2 changes: 1 addition & 1 deletion src/parser/position.hh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// A Bison parser, made by GNU Bison 3.7.6.
// A Bison parser, made by GNU Bison 3.8.2.

// Starting with Bison 3.2, this file is useless: the structure it
// used to define is now defined in "location.hh".
Expand Down