diff --git a/src/match.c b/src/match.c index 2c004c98..771ee63c 100644 --- a/src/match.c +++ b/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 { diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c index 973758eb..5c908aa5 100644 --- a/tests/unittests/torture_config.c +++ b/tests/unittests/torture_config.c @@ -2372,80 +2372,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