mirror of
https://git.libssh.org/projects/libssh.git
synced 2026-02-11 18:50:28 +09:00
CVE-2026-0967 match: Avoid recursive matching (ReDoS)
The specially crafted patterns (from configuration files) could cause
exhaustive search or timeouts.
Previous attempts to fix this by limiting recursion to depth 16 avoided
stack overflow, but not timeouts. This is due to the backtracking,
which caused the exponential time complexity O(N^16) of existing algorithm.
This is code comes from the same function from OpenSSH, where this code
originates from, which is not having this issue (due to not limiting the number
of recursion), but will also easily exhaust stack due to unbound recursion:
05bcd0cadf
This is an attempt to simplify the algorithm by preventing the backtracking
to previous wildcard, which should keep the same behavior for existing inputs
while reducing the complexity to linear O(N*M).
This fixes the long-term issue we had with fuzzing as well as recently reported
security issue by Kang Yang.
Signed-off-by: Jakub Jelen <jjelen@redhat.com>
Reviewed-by: Pavol Žáčik <pzacik@redhat.com>
This commit is contained in:
111
src/match.c
111
src/match.c
@@ -53,85 +53,70 @@
|
||||
|
||||
#include "libssh/priv.h"
|
||||
|
||||
#define MAX_MATCH_RECURSION 16
|
||||
|
||||
/*
|
||||
* Returns true if the given string matches the pattern (which may contain ?
|
||||
* and * as wildcards), and zero if it does not match.
|
||||
/**
|
||||
* @brief Compare a string with a pattern containing wildcards `*` and `?`
|
||||
*
|
||||
* This function is an iterative replacement for the previously recursive
|
||||
* implementation to avoid exponential complexity (DoS) with specific patterns.
|
||||
*
|
||||
* @param[in] s The string to match.
|
||||
* @param[in] pattern The pattern to match against.
|
||||
*
|
||||
* @return 1 if the pattern matches, 0 otherwise.
|
||||
*/
|
||||
static int match_pattern(const char *s, const char *pattern, size_t limit)
|
||||
static int match_pattern(const char *s, const char *pattern)
|
||||
{
|
||||
bool had_asterisk = false;
|
||||
const char *s_star = NULL; /* Position in s when last `*` was met */
|
||||
const char *p_star = NULL; /* Position in pattern after last `*` */
|
||||
|
||||
if (s == NULL || pattern == NULL || limit <= 0) {
|
||||
if (s == NULL || pattern == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
/* If at end of pattern, accept if also at end of string. */
|
||||
if (*pattern == '\0') {
|
||||
return (*s == '\0');
|
||||
}
|
||||
|
||||
/* Skip all the asterisks and adjacent question marks */
|
||||
while (*pattern == '*' || (had_asterisk && *pattern == '?')) {
|
||||
if (*pattern == '*') {
|
||||
had_asterisk = true;
|
||||
}
|
||||
while (*s) {
|
||||
/* Case 1: Exact match or '?' wildcard */
|
||||
if (*pattern == *s || *pattern == '?') {
|
||||
s++;
|
||||
pattern++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (had_asterisk) {
|
||||
/* If at end of pattern, accept immediately. */
|
||||
if (!*pattern)
|
||||
return 1;
|
||||
|
||||
/* If next character in pattern is known, optimize. */
|
||||
if (*pattern != '?') {
|
||||
/*
|
||||
* Look instances of the next character in
|
||||
* pattern, and try to match starting from
|
||||
* those.
|
||||
*/
|
||||
for (; *s; s++)
|
||||
if (*s == *pattern && match_pattern(s + 1, pattern + 1, limit - 1)) {
|
||||
return 1;
|
||||
}
|
||||
/* Failed. */
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* Move ahead one character at a time and try to
|
||||
* match at each position.
|
||||
/* Case 2: '*' wildcard */
|
||||
if (*pattern == '*') {
|
||||
/* Record the position of the star and the current string position.
|
||||
* We optimistically assume * matches 0 characters first.
|
||||
*/
|
||||
for (; *s; s++) {
|
||||
if (match_pattern(s, pattern, limit - 1)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
/* Failed. */
|
||||
return 0;
|
||||
}
|
||||
/*
|
||||
* There must be at least one more character in the string.
|
||||
* If we are at the end, fail.
|
||||
*/
|
||||
if (!*s) {
|
||||
return 0;
|
||||
p_star = ++pattern;
|
||||
s_star = s;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if the next character of the string is acceptable. */
|
||||
if (*pattern != '?' && *pattern != *s) {
|
||||
return 0;
|
||||
/* Case 3: Mismatch */
|
||||
if (p_star) {
|
||||
/* If we have seen a star previously, backtrack.
|
||||
* We restore the pattern to just after the star,
|
||||
* but advance the string position (consume one more char for the
|
||||
* star).
|
||||
* No need to backtrack to previous stars as any match of the last
|
||||
* star could be eaten the same way by the previous star.
|
||||
*/
|
||||
pattern = p_star;
|
||||
s = ++s_star;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Move to the next character, both in string and in pattern. */
|
||||
s++;
|
||||
/* Case 4: Mismatch and no star to backtrack to */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Handle trailing stars in the pattern
|
||||
* (e.g., pattern "abc*" matching "abc") */
|
||||
while (*pattern == '*') {
|
||||
pattern++;
|
||||
}
|
||||
|
||||
/* NOTREACHED */
|
||||
return 0;
|
||||
/* If we reached the end of the pattern, it's a match */
|
||||
return (*pattern == '\0');
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -182,7 +167,7 @@ int match_pattern_list(const char *string, const char *pattern,
|
||||
sub[subi] = '\0';
|
||||
|
||||
/* Try to match the subpattern against the string. */
|
||||
if (match_pattern(string, sub, MAX_MATCH_RECURSION)) {
|
||||
if (match_pattern(string, sub)) {
|
||||
if (negated) {
|
||||
return -1; /* Negative */
|
||||
} else {
|
||||
|
||||
@@ -2494,80 +2494,138 @@ static void torture_config_match_pattern(void **state)
|
||||
(void) state;
|
||||
|
||||
/* Simple test "a" matches "a" */
|
||||
rv = match_pattern("a", "a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", "a");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Simple test "a" does not match "b" */
|
||||
rv = match_pattern("a", "b", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", "b");
|
||||
assert_int_equal(rv, 0);
|
||||
|
||||
/* NULL arguments are correctly handled */
|
||||
rv = match_pattern("a", NULL, MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", NULL);
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern(NULL, "a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern(NULL, "a");
|
||||
assert_int_equal(rv, 0);
|
||||
|
||||
/* Simple wildcard ? is handled in pattern */
|
||||
rv = match_pattern("a", "?", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", "?");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("aa", "?", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("aa", "?");
|
||||
assert_int_equal(rv, 0);
|
||||
/* Wildcard in search string */
|
||||
rv = match_pattern("?", "a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("?", "a");
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern("?", "?", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("?", "?");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Simple wildcard * is handled in pattern */
|
||||
rv = match_pattern("a", "*", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", "*");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("aa", "*", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("aa", "*");
|
||||
assert_int_equal(rv, 1);
|
||||
/* Wildcard in search string */
|
||||
rv = match_pattern("*", "a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("*", "a");
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern("*", "*", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("*", "*");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* More complicated patterns */
|
||||
rv = match_pattern("a", "*a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", "*a");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("a", "a*", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("a", "a*");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("abababc", "*abc", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("abababc", "*abc");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("ababababca", "*abc", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("ababababca", "*abc");
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern("ababababca", "*abc*", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("ababababca", "*abc*");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Multiple wildcards in row */
|
||||
rv = match_pattern("aa", "??", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("aa", "??");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("bba", "??a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("bba", "??a");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("aaa", "**a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("aaa", "**a");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("bbb", "**a", MAX_MATCH_RECURSION);
|
||||
rv = match_pattern("bbb", "**a");
|
||||
assert_int_equal(rv, 0);
|
||||
|
||||
/* Consecutive asterisks do not make sense and do not need to recurse */
|
||||
rv = match_pattern("hostname", "**********pattern", 5);
|
||||
rv = match_pattern("hostname", "**********pattern");
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern("hostname", "pattern**********", 5);
|
||||
rv = match_pattern("hostname", "pattern**********");
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern("pattern", "***********pattern", 5);
|
||||
rv = match_pattern("pattern", "***********pattern");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("pattern", "pattern***********", 5);
|
||||
rv = match_pattern("pattern", "pattern***********");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Limit the maximum recursion */
|
||||
rv = match_pattern("hostname", "*p*a*t*t*e*r*n*", 5);
|
||||
rv = match_pattern("hostname", "*p*a*t*t*e*r*n*");
|
||||
assert_int_equal(rv, 0);
|
||||
/* Too much recursion */
|
||||
rv = match_pattern("pattern", "*p*a*t*t*e*r*n*", 5);
|
||||
rv = match_pattern("pattern", "*p*a*t*t*e*r*n*");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Regular Expression Denial of Service */
|
||||
rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("ababababababababababababababababababababab",
|
||||
"*a*b*a*b*a*b*a*b*a*b*a*b*a*b*a*b");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* A lot of backtracking */
|
||||
rv = match_pattern("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaax",
|
||||
"a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*ax");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Test backtracking: *a matches first 'a', fails on 'b', must backtrack */
|
||||
rv = match_pattern("axaxaxb", "*a*b");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Test greedy consumption with suffix */
|
||||
rv = match_pattern("foo_bar_baz_bar", "*bar");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Test exact suffix requirement (ensure no partial match acceptance) */
|
||||
rv = match_pattern("foobar_extra", "*bar");
|
||||
assert_int_equal(rv, 0);
|
||||
|
||||
/* Test multiple distinct wildcards */
|
||||
rv = match_pattern("a_very_long_string_with_a_pattern", "*long*pattern");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* ? inside a * sequence */
|
||||
rv = match_pattern("abcdefg", "a*c?e*g");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Consecutive mixed wildcards */
|
||||
rv = match_pattern("abc", "*?c");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* ? at the very end after * */
|
||||
rv = match_pattern("abc", "ab?");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("abc", "ab*?");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Consecutive stars should be collapsed or handled gracefully */
|
||||
rv = match_pattern("abc", "a**c");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("abc", "***");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Empty string handling */
|
||||
rv = match_pattern("", "*");
|
||||
assert_int_equal(rv, 1);
|
||||
rv = match_pattern("", "?");
|
||||
assert_int_equal(rv, 0);
|
||||
rv = match_pattern("", "");
|
||||
assert_int_equal(rv, 1);
|
||||
|
||||
/* Pattern longer than string */
|
||||
rv = match_pattern("short", "short_but_longer");
|
||||
assert_int_equal(rv, 0);
|
||||
}
|
||||
|
||||
/* Identity file can be specified multiple times in the configuration
|
||||
|
||||
Reference in New Issue
Block a user