Resolves: RHEL-150661 Resolves: CVE-2025-4877 Resolves: CVE-2025-4878 Resolves: CVE-2025-5351 Resolves: CVE-2025-8114 Resolves: CVE-2025-8277 Resolves: CVE-2026-0964 Resolves: CVE-2026-0965 Resolves: CVE-2026-0966 Resolves: CVE-2026-0967 Resolves: CVE-2026-0968
358 lines
13 KiB
Diff
358 lines
13 KiB
Diff
From a56c4837c9e6db6cfb7e136b0e8b8d90dbc329ea Mon Sep 17 00:00:00 2001
|
|
From: Jakub Jelen <jjelen@redhat.com>
|
|
Date: Wed, 17 Dec 2025 18:48:34 +0100
|
|
Subject: [PATCH] CVE-2026-0967 match: Avoid recursive matching (ReDoS)
|
|
MIME-Version: 1.0
|
|
Content-Type: text/plain; charset=UTF-8
|
|
Content-Transfer-Encoding: 8bit
|
|
|
|
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:
|
|
|
|
https://github.com/openssh/openssh-portable/commit/05bcd0cadf160fd4826a2284afa7cba6ec432633
|
|
|
|
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>
|
|
(cherry picked from commit a411de5ce806e3ea24d088774b2f7584d6590b5f)
|
|
---
|
|
src/match.c | 111 +++++++++++++----------------
|
|
tests/unittests/torture_config.c | 116 +++++++++++++++++++++++--------
|
|
2 files changed, 135 insertions(+), 92 deletions(-)
|
|
|
|
diff --git a/src/match.c b/src/match.c
|
|
index 3e58f733..896d87cb 100644
|
|
--- a/src/match.c
|
|
+++ b/src/match.c
|
|
@@ -43,85 +43,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');
|
|
}
|
|
|
|
/*
|
|
@@ -172,7 +157,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 70ecfb95..6dc59e48 100644
|
|
--- a/tests/unittests/torture_config.c
|
|
+++ b/tests/unittests/torture_config.c
|
|
@@ -1996,80 +1996,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
|
|
--
|
|
2.53.0
|
|
|