From 1f575a252e56faad05a2dcfa17a87441f98e521e Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:21:48 +0200 Subject: [PATCH] Fix quadratic ReDoS in skill-mention regexes The extract (SKILL_MENTION_RE) and strip (strip_re) patterns in middleware.py paired overlapping quantifiers (`[^|>]+\|?[^>]*`), which backtrack in O(n^2) on a `<$` run that never closes with `>`. Both run unconditionally on every chat completion (extract for persisted-chat skill resolution, strip before the model sees content), so a single message, RAG chunk, or tool output containing that shape pegs a CPU core in re.sub and blocks the event loop. On the default single uvicorn worker this freezes the whole instance for every user. Rewrite the optional `|label` as a non-capturing optional group so the two quantifiers no longer overlap, making both patterns linear. Capture groups and output are unchanged on well-formed `<$id|label>`, `<$id|>`, and bare `<$id>` mentions. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/open_webui/utils/middleware.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 576afe3b24..888d617d4a 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -2220,7 +2220,8 @@ def process_messages_with_output( return processed -SKILL_MENTION_RE = re.compile(r'<\$([^|>]+)\|?[^>]*>') +# non-capturing optional |label avoids quadratic ReDoS backtracking +SKILL_MENTION_RE = re.compile(r'<\$([^|>]+)(?:\|[^>]*)?>') def _get_text_parts(message: dict) -> list[str]: @@ -2244,7 +2245,8 @@ def extract_skill_ids_from_messages(messages: list[dict]) -> set[str]: def strip_skill_mentions(messages: list[dict]) -> None: """Replace <$skillId|label> mention tags with the label in message content in-place.""" - strip_re = re.compile(r'<\$[^|>]+\|?([^>]*)>') + # non-capturing optional |label avoids quadratic ReDoS backtracking + strip_re = re.compile(r'<\$[^|>]+(?:\|([^>]*))?>') for message in messages: content = message.get('content') if isinstance(content, str) and strip_re.search(content):