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.
68 lines
2.6 KiB
Diff
68 lines
2.6 KiB
Diff
# 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
|