These routes let a signed-in user start a run on a thread that belongs to someone else. We've drafted a fix that uses the same check as your other thread routes.
- Type
- BOLA
- Severity
- High
- Confidence
- 95%
- Fix
- Ready for review
Each route reads thread_id from the URL, loads that thread, and starts a run on it. The caller must be signed in, but the route doesn't check that the thread belongs to them.
So a run can read and add to another user's thread history if the caller knows or guesses its ID.
Your other /threads/{thread_id} routes filter by owner. These three load the thread by ID alone.
12 of 15 thread routes check ownership. These 3 don't.
@@ helper, shared by all three routes @@async def get_thread_for_user(session, thread_id, user): thread = await session.scalar( select(ThreadORM).where( ThreadORM.thread_id == thread_id, ThreadORM.user_id == user.identity, ) ) if not thread: raise HTTPException(404, "Thread not found") return thread @@ @router.post("/threads/{thread_id}/runs") @@async def create_run(thread_id, body, user): thread = await session.get(ThreadORM, thread_id) if not thread: raise HTTPException(404, "Thread not found") thread = await get_thread_for_user(session, thread_id, user) return await start_run(session, thread, body, user)
/runs/stream and /runs/wait.- Same owner filter your other 12 thread routes use.
- Returns 404 rather than 403, so thread IDs can't be probed.
- Includes a test: another user's request to each route gets 404.
Run it against staging with two test accounts. User B tries to start a run on user A's thread. Nothing touches production.


