Kestrel
대시보드로 돌아가기
CVE-2026-59220MEDIUM· 6.5MITRENVDGHSA대응게시일: 2026. 07. 09.수정일: 2026. 07. 24.

Open WebUI: ReDoS in skill-mention regexes causes whole-instance DoS on default config

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
6.5medium

이론적 심각도 점수

EPSS
0.4%상위 70.1%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한낮음
사용자 상호작용불필요
범위불변
영향
기밀성 영향없음
무결성 영향없음
가용성 영향높음
버전별 점수
CVSS 3.16.5MODERATE
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

상세 설명

Summary

Two regexes in backend/open_webui/utils/middleware.py that parse <$skillId|label> skill-mention tags backtrack in O(n²) on input that contains <$ followed by a long run with no closing >. Both run synchronously, on the asyncio event loop, on every chat completion with no feature gate. Because the default deployment is a single uvicorn worker, one such input pins a CPU core inside re and freezes the entire instance for all users until the worker is killed. Any authenticated user can trigger it with one chat message; it also fires accidentally on benign retrieved content (a RAG chunk or tool output) containing the pattern.

Affected versions

>= 0.9.2, < 0.10.0. Fixed in v0.10.0 (there is no 0.9.7 release).

  • SKILL_MENTION_RE (the extract pattern) has been O(n²) since v0.9.2; exploitable on 0.9.2–0.9.5 with a large input (hundreds of KB).
  • v0.9.6 added a second, far more aggressive O(n²) in the strip pattern (introduced by the "keep label as readable text" change), so on 0.9.6 a small input is enough to hang the instance.

Both are fixed by the same patch.

Affected component

backend/open_webui/utils/middleware.py (line numbers as of v0.9.6):

bash
1# line 2223 — used by extract_skill_ids_from_messages(), called unconditionally (~line 2625)
2SKILL_MENTION_RE = re.compile(r'<\$([^|>]+)\|?[^>]*>')
3
4# line 2247 — used by strip_skill_mentions(), called unconditionally (line 2662)
5strip_re = re.compile(r'<\$[^|>]+\|?([^>]*)>')

extract_skill_ids_from_messages() runs before the if all_skill_ids: block (that guard gates only skill injection, not the regex), and strip_skill_mentions() runs with no guard at all. Neither requires a skill to exist or any setting to be enabled. Both functions are plain synchronous calls inside the async process_chat_payload coroutine, so they block the event loop; with the default UVICORN_WORKERS=1 (backend/start.sh) the whole instance stalls.

Root cause

[^|>] is a subset of [^>], so the quantifier pair [^|>]+ \|? [^>]* is ambiguous: on input that never closes with >, [^|>]+ greedily consumes the tail, > fails, and the engine backtracks through every split point between [^|>]+ and [^>]* — O(n) positions each doing O(n) work. Polynomial, not exponential, but more than enough to hang a single worker on a ~100 KB input.

Proof of concept

Standalone (no Open WebUI required):

python
1import re, time
2EXTRACT = re.compile(r'<\$([^|>]+)\|?[^>]*>')
3STRIP = re.compile(r'<\$[^|>]+\|?([^>]*)>')
4for n in (8_000, 16_000, 32_000, 64_000):
5 s = '<$' + ('a' * n)
6 for name, rx in (('extract', EXTRACT), ('strip', STRIP)):
7 t = time.perf_counter(); rx.search(s)
8 print(f'n={n:>6} {name:>7} = {(time.perf_counter()-t)*1000:8.1f} ms')

Time quadruples per doubling of n (textbook O(n²)); the strip pattern runs for ~6 seconds on a 64k blob and for minutes on a ~96 KB one.

End-to-end against a live instance (default config):

  1. docker run ghcr.io/open-webui/open-webui:v0.9.6 on defaults.
  2. Log in as any user (no admin or skill setup).
  3. Send a chat message containing <$ followed by 50k+ characters with no >.
  4. One CPU core pegs in re; UI and API stop responding for every user until the worker is killed.

Patch

Rewrite the optional |label as a non-capturing optional group so the two quantifiers no longer overlap. Both patterns become linear; captures and substituted output are unchanged on well-formed <$id|label>, <$id|>, and bare <$id> mentions.

text
1SKILL_MENTION_RE = re.compile(r'<\$([^|>]+)(?:\|[^>]*)?>')
2strip_re = re.compile(r'<\$[^|>]+(?:\|([^>]*))?>')

After the patch the same hostile input returns in under 1 ms. Shipped in v0.10.0.

Credit

Reported by @Vlad-WKG, including a correct root-cause analysis and patch.

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.