Merge pull request #2736 from brandonpayton/add-regex-match-limits-and-error-reporting

Add isolated PCRE match limits as a layer of ReDoS defense
This commit is contained in:
martinhsv
2023-05-09 06:09:28 -07:00
committed by GitHub
19 changed files with 7998 additions and 7527 deletions

View File

@@ -25,12 +25,38 @@
#ifndef WITH_PCRE2
#if PCRE_HAVE_JIT
#define pcre_study_opt PCRE_STUDY_JIT_COMPILE
// NOTE: Add PCRE_STUDY_EXTRA_NEEDED so studying always yields a pcre_extra strucure
// and we can selectively override match limits using a copy of that structure at runtime.
#define pcre_study_opt PCRE_STUDY_JIT_COMPILE | PCRE_STUDY_EXTRA_NEEDED
#else
#define pcre_study_opt 0
// NOTE: Add PCRE_STUDY_EXTRA_NEEDED so studying always yields a pcre_extra strucure
// and we can selectively override match limits using a copy of that structure at runtime.
#define pcre_study_opt PCRE_STUDY_EXTRA_NEEDED
#endif
#endif
#ifdef WITH_PCRE2
class Pcre2MatchContextPtr {
public:
Pcre2MatchContextPtr()
: m_match_context(pcre2_match_context_create(NULL)) {}
Pcre2MatchContextPtr(const Pcre2MatchContextPtr&) = delete;
Pcre2MatchContextPtr& operator=(const Pcre2MatchContextPtr&) = delete;
~Pcre2MatchContextPtr() {
pcre2_match_context_free(m_match_context);
}
operator pcre2_match_context*() const {
return m_match_context;
}
private:
pcre2_match_context *m_match_context;
};
#endif
namespace modsecurity {
namespace Utils {
@@ -163,24 +189,39 @@ std::list<SMatch> Regex::searchAll(const std::string& s) const {
return retList;
}
bool Regex::searchOneMatch(const std::string& s, std::vector<SMatchCapture>& captures) const {
RegexResult Regex::searchOneMatch(const std::string& s, std::vector<SMatchCapture>& captures, unsigned long match_limit) const {
#ifdef WITH_PCRE2
Pcre2MatchContextPtr match_context;
if (match_limit > 0) {
// TODO: What if setting the match limit fails?
pcre2_set_match_limit(match_context, match_limit);
}
PCRE2_SPTR pcre2_s = reinterpret_cast<PCRE2_SPTR>(s.c_str());
pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(m_pc, NULL);
int rc = 0;
if (m_pcje == 0) {
rc = pcre2_jit_match(m_pc, pcre2_s, s.length(), 0, 0, match_data, NULL);
rc = pcre2_jit_match(m_pc, pcre2_s, s.length(), 0, 0, match_data, match_context);
}
if (m_pcje != 0 || rc == PCRE2_ERROR_JIT_STACKLIMIT) {
rc = pcre2_match(m_pc, pcre2_s, s.length(), 0, PCRE2_NO_JIT, match_data, NULL);
rc = pcre2_match(m_pc, pcre2_s, s.length(), 0, PCRE2_NO_JIT, match_data, match_context);
}
PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
#else
const char *subject = s.c_str();
int ovector[OVECCOUNT];
pcre_extra local_pce;
pcre_extra *pce = m_pce;
int rc = pcre_exec(m_pc, m_pce, subject, s.size(), 0, 0, ovector, OVECCOUNT);
if (m_pce != NULL && match_limit > 0) {
local_pce = *m_pce;
local_pce.match_limit = match_limit;
local_pce.flags |= PCRE_EXTRA_MATCH_LIMIT;
pce = &local_pce;
}
int rc = pcre_exec(m_pc, pce, subject, s.size(), 0, 0, ovector, OVECCOUNT);
#endif
for (int i = 0; i < rc; i++) {
@@ -197,12 +238,18 @@ bool Regex::searchOneMatch(const std::string& s, std::vector<SMatchCapture>& cap
#ifdef WITH_PCRE2
pcre2_match_data_free(match_data);
#endif
return (rc > 0);
return to_regex_result(rc);
}
bool Regex::searchGlobal(const std::string& s, std::vector<SMatchCapture>& captures) const {
RegexResult Regex::searchGlobal(const std::string& s, std::vector<SMatchCapture>& captures, unsigned long match_limit) const {
bool prev_match_zero_length = false;
#ifdef WITH_PCRE2
Pcre2MatchContextPtr match_context;
if (match_limit > 0) {
// TODO: What if setting the match limit fails?
pcre2_set_match_limit(match_context, match_limit);
}
PCRE2_SPTR pcre2_s = reinterpret_cast<PCRE2_SPTR>(s.c_str());
PCRE2_SIZE startOffset = 0;
@@ -213,11 +260,21 @@ bool Regex::searchGlobal(const std::string& s, std::vector<SMatchCapture>& captu
pcre2_options = PCRE2_NOTEMPTY_ATSTART | PCRE2_ANCHORED;
}
int rc = pcre2_match(m_pc, pcre2_s, s.length(),
startOffset, pcre2_options, match_data, NULL);
startOffset, pcre2_options, match_data, match_context);
PCRE2_SIZE *ovector = pcre2_get_ovector_pointer(match_data);
#else
const char *subject = s.c_str();
pcre_extra local_pce;
pcre_extra *pce = m_pce;
if (m_pce != NULL && match_limit > 0) {
local_pce = *m_pce;
local_pce.match_limit = match_limit;
local_pce.flags |= PCRE_EXTRA_MATCH_LIMIT;
pce = &local_pce;
}
int startOffset = 0;
while (startOffset <= s.length()) {
@@ -226,7 +283,12 @@ bool Regex::searchGlobal(const std::string& s, std::vector<SMatchCapture>& captu
if (prev_match_zero_length) {
pcre_options = PCRE_NOTEMPTY_ATSTART | PCRE_ANCHORED;
}
int rc = pcre_exec(m_pc, m_pce, subject, s.length(), startOffset, pcre_options, ovector, OVECCOUNT);
int rc = pcre_exec(m_pc, pce, subject, s.length(), startOffset, pcre_options, ovector, OVECCOUNT);
RegexResult regex_result = to_regex_result(rc);
if (regex_result != RegexResult::Ok) {
return regex_result;
}
#endif
if (rc > 0) {
@@ -278,7 +340,7 @@ bool Regex::searchGlobal(const std::string& s, std::vector<SMatchCapture>& captu
#ifdef WITH_PCRE2
pcre2_match_data_free(match_data);
#endif
return (captures.size() > 0);
return RegexResult::Ok;
}
int Regex::search(const std::string& s, SMatch *match) const {
@@ -340,5 +402,30 @@ int Regex::search(const std::string& s) const {
#endif
}
RegexResult Regex::to_regex_result(int pcre_exec_result) const {
if (
pcre_exec_result > 0 ||
#ifdef WITH_PCRE2
pcre_exec_result == PCRE2_ERROR_NOMATCH
#else
pcre_exec_result == PCRE_ERROR_NOMATCH
#endif
) {
return RegexResult::Ok;
} else if(
#ifdef WITH_PCRE2
pcre_exec_result == PCRE2_ERROR_MATCHLIMIT
#else
pcre_exec_result == PCRE_ERROR_MATCHLIMIT
#endif
) {
return RegexResult::ErrorMatchLimit;
} else {
// Note that this can include the case where the PCRE result was zero.
// Zero is returned if the offset vector is not large enough and can be considered an error.
return RegexResult::ErrorOther;
}
}
} // namespace Utils
} // namespace modsecurity

View File

@@ -34,6 +34,12 @@ namespace Utils {
#define OVECCOUNT 900
enum class RegexResult {
Ok,
ErrorMatchLimit,
ErrorOther,
};
class SMatch {
public:
SMatch() :
@@ -76,13 +82,15 @@ class Regex {
return (m_pc == NULL);
}
std::list<SMatch> searchAll(const std::string& s) const;
bool searchOneMatch(const std::string& s, std::vector<SMatchCapture>& captures) const;
bool searchGlobal(const std::string& s, std::vector<SMatchCapture>& captures) const;
RegexResult searchOneMatch(const std::string& s, std::vector<SMatchCapture>& captures, unsigned long match_limit = 0) const;
RegexResult searchGlobal(const std::string& s, std::vector<SMatchCapture>& captures, unsigned long match_limit = 0) const;
int search(const std::string &s, SMatch *match) const;
int search(const std::string &s) const;
const std::string pattern;
private:
RegexResult to_regex_result(int pcre_exec_result) const;
#if WITH_PCRE2
pcre2_code *m_pc;
int m_pcje;