fix(streaming): reject hostile session SSE cursors (#4812)

This commit is contained in:
Rod Boev 2026-07-06 11:25:59 -04:00
parent 3e53ef505c
commit 9b3538f1a6
2 changed files with 45 additions and 1 deletions

View file

@ -337,6 +337,17 @@ def read_session_run_events(
sid = _validate_id(session_id, "session_id")
cursor_run_id, cursor_seq = _parse_run_journal_event_id(after_event_id)
raw_cursor = str(after_event_id or "").strip()
if raw_cursor and cursor_run_id is not None:
try:
cursor_run_id = _validate_id(cursor_run_id, "run_id")
except ValueError:
cursor_seq = None
if raw_cursor:
try:
if int(raw_cursor.rsplit(":", 1)[-1]) < 0:
cursor_seq = None
except (TypeError, ValueError):
pass
if raw_cursor and (cursor_run_id is None or cursor_seq is None):
return {
"session_id": sid,
@ -360,7 +371,8 @@ def read_session_run_events(
if cursor_entry is None:
summary = find_run_summary(cursor_run_id or "", session_dir=session_dir) if cursor_run_id else None
status = "cursor_run_missing"
if summary and str(summary.get("session_id") or "") != sid:
summary_sid = str(summary.get("session_id") or "").strip() if summary else ""
if summary_sid and summary_sid != sid:
status = "cursor_session_mismatch"
return {
"session_id": sid,

View file

@ -163,6 +163,38 @@ def test_session_journal_distinguishes_missing_and_foreign_cursor(tmp_path):
assert foreign["status"] == "cursor_session_mismatch"
def test_session_journal_rejects_hostile_cursor_without_replay(tmp_path, monkeypatch):
append_run_event(
"session_1",
"run_a",
"token",
{"text": "a1"},
session_dir=tmp_path,
seq=1,
created_at=100.0,
)
monkeypatch.setattr(
"api.run_journal.find_run_summary",
lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("invalid cursor should not scan summaries")),
)
malformed = read_session_run_events("session_1", after_event_id="bad/run:1", session_dir=tmp_path)
negative = read_session_run_events("session_1", after_event_id="run_a:-5", session_dir=tmp_path)
assert malformed["status"] == "cursor_invalid"
assert malformed["events"] == []
assert negative["status"] == "cursor_invalid"
assert negative["events"] == []
def test_session_journal_blank_summary_session_stays_missing(tmp_path, monkeypatch):
monkeypatch.setattr("api.run_journal.find_run_summary", lambda *_args, **_kwargs: {"session_id": ""})
replay = read_session_run_events("session_1", after_event_id="run_missing:1", session_dir=tmp_path)
assert replay["status"] == "cursor_run_missing"
def test_session_route_prefers_last_event_id_then_query_fallback(monkeypatch):
import api.routes as routes