feat: add UNDF-2026-000001266 (salt-0004) and UNDF-2026-000001267 (ansible-0004) CWE-1333
Salt pcre/grain_pcre targeting passes user-controlled regex to re.match/re.compile without timeout. Ansible inventory manager passes tilde-prefixed host patterns directly to re.compile. Both fixed with ThreadPoolExecutor 1s timeout wrapper.
This commit is contained in:
parent
7f280e8187
commit
f74250d8a3
3 changed files with 188 additions and 1 deletions
|
|
@ -1263,5 +1263,7 @@
|
|||
"lean4-0004": "UNDF-2026-000001262",
|
||||
"lean4-0005": "UNDF-2026-000001263",
|
||||
"lean4-0006": "UNDF-2026-000001264",
|
||||
"lean4-0007": "UNDF-2026-000001265"
|
||||
"lean4-0007": "UNDF-2026-000001265",
|
||||
"salt-0004": "UNDF-2026-000001266",
|
||||
"ansible-0004": "UNDF-2026-000001267"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
# UNDF: UNDF-2026-000001267
|
||||
# CWE-1333: Inefficient Regular Expression Complexity. ReDoS via ~-prefix inventory pattern.
|
||||
#
|
||||
# Defect: re.compile(pattern[1:]) called with user-supplied ~-prefix inventory pattern.
|
||||
# Passes raw user pattern directly to the backtracking NFA engine.
|
||||
# Attack vector: ansible-playbook --limit '~^(a+)+$' or ~-prefixed host in inventory/playbook.
|
||||
# Stalls the controller process.
|
||||
#
|
||||
# Fix: wrap re.compile/re.search with a ThreadPoolExecutor 1s timeout. Fail-closed.
|
||||
# A pattern that times out or fails to compile returns no matches (empty list).
|
||||
#
|
||||
# Complexity gate: pattern ~^(a+)+$ against input 'a'*25+'b' must complete in <3s (vs infinite without fix).
|
||||
--- a/lib/ansible/inventory/manager.py
|
||||
+++ b/lib/ansible/inventory/manager.py
|
||||
@@ -1,7 +1,9 @@
|
||||
import fnmatch
|
||||
import re
|
||||
import typing as t
|
||||
+import concurrent.futures
|
||||
+import logging
|
||||
+
|
||||
+log = logging.getLogger(__name__)
|
||||
+
|
||||
+_REGEX_EXECUTOR = concurrent.futures.ThreadPoolExecutor(max_workers=2)
|
||||
+_REGEX_TIMEOUT = 1.0 # seconds
|
||||
+
|
||||
+
|
||||
+def _safe_regex_compile(pattern):
|
||||
+ """
|
||||
+ Compile a regex pattern in a worker thread with a 1s timeout.
|
||||
+ Returns the compiled pattern on success, None on timeout or re.error.
|
||||
+ Fail-closed: a pattern that stalls or fails returns None.
|
||||
+ """
|
||||
+ try:
|
||||
+ future = _REGEX_EXECUTOR.submit(re.compile, pattern)
|
||||
+ return future.result(timeout=_REGEX_TIMEOUT)
|
||||
+ except concurrent.futures.TimeoutError:
|
||||
+ log.warning(
|
||||
+ "inventory regex compile timed out after %ss: pattern=%r",
|
||||
+ _REGEX_TIMEOUT, pattern
|
||||
+ )
|
||||
+ return None
|
||||
+ except re.error as exc:
|
||||
+ log.warning("invalid inventory regex %r: %s", pattern, exc)
|
||||
+ return None
|
||||
+ except Exception as exc: # pylint: disable=broad-except
|
||||
+ log.warning("inventory regex error pattern=%r: %s", pattern, exc)
|
||||
+ return None
|
||||
@@ -330,13 +370,15 @@ class InventoryManager:
|
||||
def _match_list(self, items, pattern_str):
|
||||
# compile patterns
|
||||
try:
|
||||
if not pattern_str[0] == '~':
|
||||
pattern = re.compile(fnmatch.translate(pattern_str))
|
||||
else:
|
||||
- pattern = re.compile(pattern_str[1:])
|
||||
+ pattern = _safe_regex_compile(pattern_str[1:])
|
||||
+ if pattern is None:
|
||||
+ raise AnsibleError('Regex pattern timed out or invalid: %s' % pattern_str)
|
||||
except Exception:
|
||||
raise AnsibleError('Invalid host list pattern: %s' % pattern_str)
|
||||
|
||||
# apply patterns
|
||||
results = []
|
||||
for item in items:
|
||||
if pattern.match(item):
|
||||
results.append(item)
|
||||
return results
|
||||
117
defects/salt/patch/salt-0004-pcre-redos-safe-wrapper.patch
Normal file
117
defects/salt/patch/salt-0004-pcre-redos-safe-wrapper.patch
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# UNDF: UNDF-2026-000001266
|
||||
# CWE-1333: Inefficient Regular Expression Complexity. ReDoS via user-controlled grain_pcre pattern.
|
||||
#
|
||||
# Defect: re.match/re.compile receives tgt from job payload when tgt_type=pcre or grain_pcre.
|
||||
# A crafted pattern like ^(a+)+$ against adversarial input causes exponential backtracking.
|
||||
# Attack vector: authenticated Salt API caller POST /run with tgt_type=pcre.
|
||||
# Stalls the master process indefinitely.
|
||||
#
|
||||
# Fix: route re.match/re.compile through a ThreadPoolExecutor worker with 1s timeout.
|
||||
# Fail-closed. timeout or re.error returns False (minion excluded from target).
|
||||
#
|
||||
# Complexity gate: pattern ^(a+)+$ against input 'a'*25+'b' must complete in <3s (vs infinite without fix).
|
||||
--- a/salt/matchers/pcre_match.py
|
||||
+++ b/salt/matchers/pcre_match.py
|
||||
@@ -1,17 +1,56 @@
|
||||
"""
|
||||
This is the default pcre matcher.
|
||||
"""
|
||||
|
||||
import re
|
||||
+import concurrent.futures
|
||||
+import logging
|
||||
+
|
||||
+log = logging.getLogger(__name__)
|
||||
+
|
||||
+_PCRE_EXECUTOR = concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
||||
+_PCRE_TIMEOUT = 1.0 # seconds
|
||||
+
|
||||
+
|
||||
+def _safe_pcre_match(pattern, text, timeout=_PCRE_TIMEOUT):
|
||||
+ """
|
||||
+ Run re.match(pattern, text) in a worker thread with a timeout.
|
||||
+ Returns the match object on success, False on timeout or re.error.
|
||||
+ Fail-closed: a pattern that stalls or fails to compile returns False.
|
||||
+ """
|
||||
+ try:
|
||||
+ future = _PCRE_EXECUTOR.submit(re.match, pattern, text)
|
||||
+ return future.result(timeout=timeout)
|
||||
+ except concurrent.futures.TimeoutError:
|
||||
+ log.warning(
|
||||
+ "pcre match timed out after %ss: pattern=%r target=%r",
|
||||
+ timeout, pattern, text
|
||||
+ )
|
||||
+ return False
|
||||
+ except re.error as exc:
|
||||
+ log.warning("invalid pcre pattern %r: %s", pattern, exc)
|
||||
+ return False
|
||||
+ except Exception as exc: # pylint: disable=broad-except
|
||||
+ log.warning("pcre match error pattern=%r: %s", pattern, exc)
|
||||
+ return False
|
||||
|
||||
|
||||
def match(tgt, opts=None, minion_id=None):
|
||||
"""
|
||||
Returns true if the passed pcre regex matches
|
||||
"""
|
||||
if not opts:
|
||||
opts = __opts__
|
||||
if not minion_id:
|
||||
minion_id = opts.get("id")
|
||||
|
||||
- return bool(re.match(tgt, minion_id))
|
||||
+ return bool(_safe_pcre_match(tgt, minion_id))
|
||||
--- a/salt/utils/minions.py
|
||||
+++ b/salt/utils/minions.py
|
||||
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
import fnmatch
|
||||
import logging
|
||||
+import concurrent.futures
|
||||
+from salt.matchers.pcre_match import _safe_pcre_match, _PCRE_EXECUTOR, _PCRE_TIMEOUT
|
||||
@@ -320,11 +322,17 @@ class CkMinions:
|
||||
def _check_pcre_minions(
|
||||
self, expr, greedy, minions=None
|
||||
): # pylint: disable=unused-argument
|
||||
"""
|
||||
Return the minions found by looking via regular expressions
|
||||
"""
|
||||
- reg = re.compile(expr)
|
||||
-
|
||||
if not minions:
|
||||
minions = self._pki_minions()
|
||||
|
||||
- return {
|
||||
- "minions": [m for m in minions if reg.match(m)],
|
||||
- "missing": [],
|
||||
- }
|
||||
+ try:
|
||||
+ future = _PCRE_EXECUTOR.submit(re.compile, expr)
|
||||
+ reg = future.result(timeout=_PCRE_TIMEOUT)
|
||||
+ except concurrent.futures.TimeoutError:
|
||||
+ log.warning(
|
||||
+ "pcre compile timed out after %ss: expr=%r", _PCRE_TIMEOUT, expr
|
||||
+ )
|
||||
+ return {"minions": [], "missing": []}
|
||||
+ except re.error as exc:
|
||||
+ log.warning("invalid pcre expr %r: %s", expr, exc)
|
||||
+ return {"minions": [], "missing": []}
|
||||
+
|
||||
+ return {
|
||||
+ "minions": [m for m in minions if _safe_pcre_match(expr, m)],
|
||||
+ "missing": [],
|
||||
+ }
|
||||
--- a/salt/utils/data.py
|
||||
+++ b/salt/utils/data.py
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
import fnmatch
|
||||
+from salt.matchers.pcre_match import _safe_pcre_match
|
||||
@@ -145,9 +146,9 @@ def subdict_match(
|
||||
if regex_match:
|
||||
try:
|
||||
- return re.match(pattern, target)
|
||||
+ return _safe_pcre_match(pattern, target)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
log.error("Invalid regex '%s' in match", pattern)
|
||||
return False
|
||||
Loading…
Add table
Add a link
Reference in a new issue