
POC for CVE-2026-30950 which allows session hijacking in AutoGpt
Proof-of-concept demonstration for CVE-2026-30950, an authenticated IDOR (Missing Authorization) in the AutoGPT Platform that lets any logged-in user reassign — and thus hijack — any other user's chat session via a single PATCH request, with no prior access to the session.
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:L)autogpt-platform-backend >= 0.6.36The chat-session API exposes a route that lets a user attach their own
account to a session record. The route is gated by JWT authentication
but performs no check that the caller currently owns the session
being modified — only that they are some authenticated user. A direct
call with the victim's session_id and the attacker's JWT transfers
ownership of the session to the attacker.
The vulnerability has three layers and the bypass is established at each one:
# autogpt_platform/backend/backend/api/features/chat/routes.py:753-776
@router.patch(
"/sessions/{session_id}/assign-user",
dependencies=[Security(auth.requires_user)],
status_code=200,
)
async def session_assign_user(
session_id: str,
user_id: Annotated[str, Security(auth.get_user_id)],
) -> dict:
await chat_service.assign_user_to_session(session_id, user_id)
return {"status": "ok"}
The route accepts any authenticated caller. It hands off to the service:
# autogpt_platform/backend/backend/copilot/service.py:291-303
async def assign_user_to_session(session_id: str, user_id: str) -> ChatSessionInfo:
session = await get_chat_session(session_id, None) # ← user_id=None
if not session:
raise NotFoundError(f"Session {session_id} not found")
session.user_id = user_id
session = await upsert_chat_session(session)
return session
The service deliberately passes user_id=None to the data accessor,
instead of forwarding the caller's user ID. The data accessor then
treats None as admin-mode and skips the ownership filter:
# autogpt_platform/backend/backend/copilot/model.py:355-366
session = await _get_session_from_cache(session_id)
if session:
if user_id is not None and session.user_id != user_id:
logger.warning(f"Session {session_id} user id mismatch")
return None
return session
When user_id is None, the conjunction short-circuits and the mismatch
check is never performed — any session is returned to any caller. The
service then overwrites session.user_id with the caller's ID and
caches the result in Redis, so subsequent lookups by the original owner
are rejected by the same mismatch check that was just bypassed.
Any AutoGPT Platform instance running autogpt-platform-backend
>= 0.6.36 and < 0.6.51 with the chat (copilot) feature enabled
is exploitable. The exploit requires:
Per-session impact, escalating with each hijacked session:
The CVSS score (C:H / I:N / A:L) reflects the per-session scope: high confidentiality impact on hijacked sessions, no integrity damage to pre-existing message contents, and an availability impact (lockout) on the legitimate owner.
This repository ships one POC, covering the direct trigger: a
single PATCH request from an attacker's JWT against a known
session_id. The setup script creates two users (attacker + victim)
in a freshly-bootstrapped AutoGPT stack and verifies the preconditions
before declaring ready.
setup/ — Docker Compose environment.
setup.sh clones AutoGPT at the last vulnerable tag
(autogpt-platform-beta-v0.6.50) into setup/AutoGPT-src/, brings
up the minimum services from upstream's docker-compose.yml
(rest_server, copilot_executor, database_manager, migrate,
plus their transitive deps: Postgres, Redis, RabbitMQ, Supabase
Kong + GoTrue), creates two test users, and verifies the
vulnerable endpoint is routed.teardown.sh brings down the stack and wipes volumes.pocs/session_hijack.py — Self-contained exploit. Logs in as
victim and attacker, creates a session as the victim, confirms the
attacker has no pre-exploit read access, fires the single PATCH
request, then proves ownership has transferred and the victim is
locked out.
Prerequisites: Docker, Git, Python 3.10+, and uv.
cd setup
./setup.sh
The first run clones AutoGPT at the vulnerable tag and builds the backend image (~3-5 min). Subsequent runs are quick. When setup completes it prints the ready-to-paste POC invocation.
Run the POC:
uv run --no-project --with requests \
pocs/session_hijack.py \
--api-url http://localhost:58006 \
--auth-url http://localhost:58000 \
--attacker-email [email protected] \
--attacker-password 'Attacker123!' \
--victim-email [email protected] \
--victim-password 'Victim123!'
Why the high ports? Upstream's compose hardcodes container names (
supabase-db,rabbitmq, ...) and publishes default ports (5432, 8000, ...). To coexist with any other Supabase / Postgres / RabbitMQ stacks the user might already be running,setup/docker-compose.override.ymlrenames the containers, isolates the networks under a unique project name (autogpt-cve-2026-30950), and bumps the two externally-exposed ports into the 58000 range. The other internal services bind only to the project's docker network.
Expected output ends with:
[PASS] AUTHENTICATED SESSION HIJACK CONFIRMED
Tear down:
cd setup
./teardown.sh
The cloned AutoGPT source in setup/AutoGPT-src/ is preserved across
teardowns so re-runs don't re-download. To wipe it entirely, delete
setup/AutoGPT-src/.