ulysses/routes
nopoz c098355778
fix(security): prevent ReDoS in LLM-output tool/think parsers (#4704)
* fix(security): prevent ReDoS in LLM-output tool/think parsers

The regexes that parse untrusted model output in text_helpers.py and
tool_parsing.py are delimiter-bounded with a lazy [\s\S]*? (or an
ambiguous (\s+[^>]*)?). Applied with re.sub/re.finditer over a whole
response, they degrade to O(n^2) when the closing delimiter is absent:
the engine rescans to end-of-string from every opener. Model output is
untrusted, so a prompt-injected or malicious model can stall the agent
loop with many unclosed openers (measured ~25s on a 60KB <thought flood).

- text_helpers.py: replace ambiguous <thought(\s+[^>]*)?> with
  <thought([^>]*)> (identical capture, no \s+/[^>]* overlap); skip the
  Gemma <|channel>...<channel|> subs when no <channel|> closer is present.
- tool_parsing.py: gate _TOOL_CALL_RE, _XML_TOOL_CALL_RE and _TOOL_CODE_RE
  (in parse_tool_blocks and strip_tool_blocks) on a cheap presence check
  for their closing delimiter. With no closer the regex cannot match, so
  skipping is equivalent; only the wasted O(n^2) rescan is removed.

Resolves CodeQL py/polynomial-redos #230, #231, #232, #233, #235, #236,
#524. The _XML_OPEN_TOOL_CALL_RE alerts (#234, #477) are false positives
(its greedy [\s\S]*\Z is linear) and left untouched.

* fix(security): close ReDoS gaps in tool/think parsers from review

Addresses two review findings on the closer-guard approach:

- Whole-string "closer exists?" checks were bypassable: a stale closer
  before an opener flood, or a closer with no reachable inner `}`, kept
  the guard true while every opener still rescanned to end-of-string
  (O(n^2)). Replace the substring guards with `_iter_delimited`, a
  forward-only scan that pairs each opener with a *later* closer and
  stops once none is reachable (O(n)). `parse_tool_blocks` and
  `strip_tool_blocks` (via `_strip_delimited`) both use it for the
  [TOOL_CALL], <tool_call>/<function_call>, and <tool_code> formats.
  Verified equivalent to the original regexes on well-formed inputs.

- `<thought([^>]*)>` dropped the tag-name boundary and corrupted
  unrelated tags (`<thoughtful>` -> `<thinkful>`). Use `<thought(\s[^>]*)?>`:
  the single fixed `\s` keeps the pattern linear (no `\s+`/`[^>]*`
  overlap) while restoring the boundary; capture is byte-for-byte
  identical for real `<thought ...>` openers.

Adds regressions for stale-closer-before-opener, closer-present-without-
inner-brace, and the <thoughtful>/<thoughts> passthrough.

* fix(security): close Gemma channel ReDoS guard flagged in review

vdmkenny noted the same bypassable whole-string guard remained in
text_helpers.py: `if "<channel|>" in out.lower()` gating the Gemma
thought/response channel subs. A stale `<channel|>` before a
`<|channel>thought` opener flood keeps the guard true while every opener
still rescans to end-of-string (measured ~7.3s at 4k openers).

Replace it with `_sub_delimited`, the same forward-only scan used for the
tool-call parsers: pair each opener with a later closer, stop when none is
reachable (O(n)). Verified output-equivalent to the original capture regexes
on well-formed multi-channel inputs; the stale-closer case now runs in <2ms.
Adds a regression for stale-closer-before-opener on the Gemma path.

* fix(security): harden strip_think() think-tag ReDoS flagged in review

The earlier fixes hardened normalize_thinking_markup and the delimiter
scanners, but the production entrypoint strip_think() still ran
_THINK_CLOSED_RE / _THINK_ATTR_RE / _THINK_OPEN_RE (and the stray-tag
_THINK_TAG_RE) over untrusted model output. Those kept the same ReDoS
shapes: the lazy `<open>[\s\S]*?</close>` rescanned to end-of-string from
every opener, and `(?:\s+[^>]*)?` / `[^>]*` attribute scans ran to
end-of-string from every opener on a "many openers, no closer" flood. On
the prior head, malformed `<think` / `<thinking` / `<thought` floods took
6-14s through strip_think(). The shipped `<thought>` normalization had the
same residual: the single-opener case was linear but an opener flood was
still O(n^2) (~4.4s).

- Replace the lazy multi-pass _THINK_CLOSED_RE loop with the existing
  forward-only _sub_delimited scan (pair each opener with the first
  reachable closer, stop when none is reachable). One pass collapses
  sequential and nested blocks as before.
- Bound every opener/stray-tag attribute scan at `<` (`[^<>]` not `[^>]`)
  so a no-`>` opener flood can't drive a single match attempt to
  end-of-string. Identical capture for well-formed think/thought tags.
- email_helpers._strip_think: compute had_think from the single linear
  _THINK_TAG_RE instead of the lazy closed/open `.search()` calls, which
  had the same O(n^2) on the email reply/summary/extraction paths.

All flood variants now finish in <10ms (were 6-14s). Output verified
byte-for-byte identical to the prior implementation over a 34-case corpus
(nested, mismatched, attr, uppercase, Gemma, prose, prompt-echo). Adds
strip_think() timing regressions for malformed openers, opener floods
(all three tag names), the closed-opener flood, and the malformed-closer
flood.

* docs: trim verbose comments in think-tag ReDoS fix
2026-06-27 10:12:28 -07:00
..
__init__.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
_validators.py fix(hwfit): validate remote SSH detection targets (#3718) 2026-06-11 00:43:49 +02:00
admin_wipe_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
api_token_routes.py fix(api): normalize non-object JSON bodies to empty dict in token PATCH (#3976) 2026-06-15 18:05:15 +01:00
assistant_routes.py fix(auth): centralize password and username validation constants (#4120) 2026-06-16 09:52:15 +02:00
auth_routes.py fix(auth): tie remember-me cookie lifetime to TOKEN_TTL (#4472) 2026-06-18 21:15:48 +02:00
backup_routes.py fix: backup import dropping a user's skill on cross-tenant title/id collision (#2057) 2026-06-09 08:04:22 +02:00
calendar_routes.py fix(calendar): keep imported events with non-positive duration visible (#4484) 2026-06-27 16:52:40 +02:00
chat_helpers.py Merge origin/dev into main 2026-06-21 11:08:50 +00:00
chat_routes.py fix(security): redact credential-bearing URLs and PII from logs (#4750) 2026-06-22 23:12:39 +02:00
chatgpt_subscription_routes.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00
cleanup_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
codex_routes.py fix(security): gate codex cookbook routes behind admin check for cookie sessions (#4554) 2026-06-27 14:09:32 +01:00
compare_routes.py fix(endpoint): scope secondary endpoint lookups by owner 2026-06-08 11:51:55 +01:00
contacts_routes.py fix: vCard parser drops folded continuation lines, corrupting emails (#1870) 2026-06-27 14:41:57 +01:00
cookbook_helpers.py fix(cookbook): treat local Windows as Windows for serve commands (#3975) 2026-06-26 13:13:01 +01:00
cookbook_output.py fix(cookbook): report dead finished downloads as completed instead of stopped (#4025) 2026-06-15 15:26:55 +09:00
cookbook_routes.py fix(cookbook): treat local Windows as Windows for serve commands (#3975) 2026-06-26 13:13:01 +01:00
copilot_routes.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00
device_flow.py feat: add ChatGPT Subscription provider (#2876) 2026-06-08 10:19:18 +02:00
diagnostics_routes.py feat(ui): add real-time diagnostic logs console (#974) 2026-06-15 10:32:51 +02:00
document_helpers.py fix: document read fails with 403 when auth is disabled (#4623) 2026-06-22 21:01:11 +02:00
document_routes.py fix: document read fails with 403 when auth is disabled (#4623) 2026-06-22 21:01:11 +02:00
editor_draft_routes.py Ignore invalid editor draft payloads (#1533) 2026-06-03 14:07:03 +09:00
email_helpers.py fix(security): prevent ReDoS in LLM-output tool/think parsers (#4704) 2026-06-27 10:12:28 -07:00
email_pollers.py fix: email poller marks calendar extraction processed on LLM failure (#4622) 2026-06-23 20:32:30 +02:00
email_routes.py fix(email): validate IMAP/SMTP ports instead of crashing with 500 (#4464) 2026-06-26 20:32:56 +02:00
embedding_routes.py feat(paths): abstract runtime path logic for frozen distribution packages (#969) 2026-06-15 17:44:10 +01:00
emoji_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
font_routes.py Keep compact font family names together (#1263) 2026-06-03 14:24:30 +09:00
gallery_helpers.py fix(gallery): fail closed for null-user owner scope (#3613) 2026-06-09 20:20:21 +02:00
gallery_routes.py fix(gallery): confine gallery image path resolution (#4352) 2026-06-16 03:28:09 +01:00
history_routes.py fix(sessions): copy message metadata when forking a session (#3409) 2026-06-08 20:49:15 +02:00
hwfit_routes.py CodeQL hardening for cookbook sync 2026-06-22 02:39:18 +00:00
mcp_routes.py fix(mcp): share oauth redirect URI (#4087) 2026-06-15 15:15:53 +09:00
memory_routes.py fix(routes): normalize session owner fallback helpers (#4313) 2026-06-16 06:07:42 +01:00
model_routes.py fix(models): accept bare-list /models responses (Together AI) (#4761) 2026-06-27 16:25:15 +01:00
note_routes.py fix(security): redact credential-bearing URLs and PII from logs (#4750) 2026-06-22 23:12:39 +02:00
personal_routes.py fix(personal): scope RAG file delete to the caller's own upload dir (#4602) 2026-06-20 00:50:15 +02:00
prefs_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
preset_routes.py fix(presets): scope expand-prompt model resolution to owner (#3477) 2026-06-08 21:12:02 +02:00
research_routes.py refactor(auth): centralize the internal-tool pseudo-username into a constant (#4333) 2026-06-16 13:13:00 +02:00
search_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
session_routes.py fix(auth): clean up rename and null-owner ownership (#4340) 2026-06-16 03:33:02 +01:00
shell_routes.py fix: Real-ESRGAN install + Cookbook deps-panel crash on the Python 3.14 image (#4694) 2026-06-23 19:31:00 +02:00
signature_routes.py Constrain signature uploads to PNG data (#2844) 2026-06-05 13:17:43 +02:00
skills_routes.py Skills test: set explicit max_tokens=4096 instead of 0 2026-06-13 23:09:15 +09:00
stt_routes.py refactor(uploads): centralize upload byte-limits in upload_limits.py (#3364) (#3518) 2026-06-09 01:24:30 +02:00
task_routes.py refactor(auth): centralize the internal-tool pseudo-username into a constant (#4333) 2026-06-16 13:13:00 +02:00
tts_routes.py Odysseus v1.0 2026-05-31 23:58:26 +09:00
upload_routes.py Cookbook launch and gallery upload fixes 2026-06-22 01:49:15 +00:00
vault_routes.py refactor(constants): single source of truth for data dir (#3368) 2026-06-08 09:58:52 +02:00
webhook_routes.py fix(models): accept bare-list /models responses (Together AI) (#4761) 2026-06-27 16:25:15 +01:00
workspace_routes.py feat(agent): confine agent file/shell tools to a selectable workspace (#3665) 2026-06-11 18:17:54 +02:00