MaxKB Shipped a Shell Tool Nobody Asked For — and Forgot to Ask Before Running It
NVD published CVE-2026-77521 on September 21 with a CVSS 3.1 base score of 10.0 — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, the arithmetic maximum. The affected product is MaxKB, the 1Panel-dev open-source enterprise agent platform with roughly 22,900 GitHub stars. The advisory, GHSA-f36j-f34j-h3rx, credits Lasso Security and is fixed in 2.10.5-lts.
The one-line summary — "prompt-injectable agent can lead to command execution" — undersells what is actually documented. This is not a bug in a shell tool. It is a shell tool that was never a product decision at all, attached to every agent as a side effect of how a framework was called.
The vulnerable line is a constructor argument
The mechanism is visible in four lines of the fix-tagged source. In apps/application/flow/tools.py at tag v2.10.3-lts, MaxKB builds its agent like this:
agent = create_deep_agent(
model=chat_model,
backend=SandboxShellBackend(root_dir=temp_dir, virtual_mode=True),
skills=["/skills"],
tools=tools,
system_prompt=system_prompt,
interrupt_on={"write_file": False, "read_file": False, "edit_file": False},
checkpointer=checkpointer,
)
Two details carry the entire vulnerability. First, per the advisory, the deepagents library auto-injects execute alongside filesystem tools whenever a shell backend is passed to create_deep_agent. The tools= list holds the operator's chosen tools; execute is not in it and was never selected by anyone. It arrives because a backend object was constructed. MaxKB does not pass excluded_tools, so nothing removes it.
Second, look at what interrupt_on covers: write_file, read_file, edit_file. The human-in-the-loop map enumerates the three filesystem operations and omits the one that runs commands. The dictionary is the approval gate, and the most dangerous capability is simply not a key in it. Writing a file requires a decision about approval; executing a shell command does not, because the developer writing that gate was thinking about the tools they knew they had added.
The trigger condition is unremarkable. At base_chat_node.py:409, any assistant with at least one tool, MCP tool, skill, or sub-application attached routes through this agent path. That is the normal way MaxKB is used as an agent platform. A capability-less chatbot is not affected — which means the exposure grows precisely as an operator adopts the product's main feature.
Why the score is 10.0 and not 9.8
Perfect-10 CVSS scores are rare enough to deserve scrutiny, and the advisory shows its work. The two unusual components are PR:N and S:C.
No privileges required applies to the public or embedded assistant case. MaxKB supports anonymous embedded bots; if such a bot has a tool attached, the attacker is a visitor typing into a chat widget. In a purely authenticated deployment the vector is narrower, but it does not disappear — the advisory notes the same path is reachable through indirect prompt injection via ingested RAG content or uploaded documents, where the attacker never touches the chat interface at all and only needs to control a document the assistant reads.
Scope changed is the more interesting claim. The vulnerable component is the agent sandbox; the impacted authority is the host OS, other tenants, and reachable internal services. That holds only if containment actually fails, and the advisory documents two distinct ways it does.
Two deployment shapes, two different failures
Containment hangs entirely on MAXKB_SANDBOX, and the code default is off. In sandbox_shell.py, _enable_sandbox = bool(int(CONFIG.get("SANDBOX", 0))) — and SANDBOX is absent from Config.defaults in apps/maxkb/conf.py. On a source or bare-metal install that does not explicitly set the variable, the wrapper is skipped and the command reaches subprocess.run(command, shell=True) on the host as the application user. That is straightforward remote code execution.
The container case is subtler and, in some ways, worse. The official image sets MAXKB_SANDBOX=1 in the base image, so execute is wrapped as env -i LD_PRELOAD=.../sandbox.so ... gosu sandbox <cmd>. But the whole string runs under shell=True, and neither Dockerfile carries a USER directive — the container runs as UID 0. The wrapper drops privileges only for the bare command token. Shell metacharacters and redirections are evaluated by the outer root shell, outside gosu. In Lasso's transcript a redirect wrote a root-owned file while the inner id token was correctly blocked by sandbox.so. The sandbox stopped the thing it was looking at and let the shell syntax around it through.
This is worth stating plainly: a defense that filters the command but not the shell that interprets it is not a weak boundary, it is a boundary in the wrong place. The fix commit, 594f50f (July 9), touches exactly one file — sandbox_shell.py — and adds shlex plus a hand-written parser that walks quotes, backticks, escapes, and command-substitution depth to split the command before building the sandboxed invocation. Several hundred lines of shell-grammar parsing now stand between the model and the root shell.
The honest caveat about model alignment
One line in the advisory deserves more attention than it will get. Lasso reports that a blatant "proof"-style filename was refused by the model, while benign, operations-flavored phrasing executed. Their conclusion — "model alignment is a partial speed-bump, not a control" — is the correct reading, and it cuts against a common defensive assumption.
If a system's safety depends on the model declining to use a capability it has been handed, that safety is a function of phrasing, of which model the operator plugged in, and of whether the instruction arrived as an obvious attack or as a plausible support request. None of those are controls an operator can audit. This is the same lesson as the AgentCore work we covered, where a poisoned support ticket drove a default shell tool into reading its own process memory, and the same one the tool-allowlisting research reached from the defensive side: enforcement has to sit outside the model, because the model cannot reliably distinguish instructions it should follow from text it merely read.
What the patch did not change
Here is the part operators should sit with. We compared tools.py across v2.10.3-lts, v2.10.5-lts, and the current v2.10.6-lts. The create_deep_agent call is byte-for-byte identical in all three. SandboxShellBackend is still the default backend for every tool-attached assistant. excluded_tools is still absent. interrupt_on still lists three filesystem operations and still has no execute key.
Lasso's first two remediation suggestions were to stop applying the shell backend by default and to gate execute with human-in-the-loop approval. Neither was adopted. What shipped is suggestion five — fix the wrapper so metacharacters cannot escape gosu. That is a real and necessary fix, and it closes the documented container escape. It also means the post-patch security model is: an ungated shell tool the operator never enabled, still auto-attached to every capable assistant, still drivable by injected content, now contained by a newly written shell parser and an LD_PRELOAD sandbox that must be switched on.
On a source deployment with MAXKB_SANDBOX unset, the parser hardens a wrapper that the code path skips entirely. Upgrading is necessary. It is not, by itself, the same as the capability being gated.
Not an isolated finding
CVE-2026-77521 arrived inside an unusually large disclosure wave. MaxKB's repository advisory list now holds 49 published advisories for 2026, 40 of them with assigned CVEs: three critical, seven high, 33 medium, six low. On September 2 and 3 alone, more than twenty landed at once — cross-knowledge IDOR, missing per-tool authorization, expired API keys still accepted on /chat/api/mcp, a hardcoded default Django SECRET_KEY, an unrated-limited password-reset code, and two further sandbox escapes (CVE-2026-79918, unhooked fexecve; CVE-2026-79919, a dlopen stack-check bypass).
The second critical in that batch is instructive alongside the first. CVE-2026-79916 (CVSS 9.1) covers AWS Bedrock model creation writing user-controlled credential fields straight into /root/.aws/credentials by f-string interpolation, with no newline filtering. An authenticated workspace member — the default USER role can create models — appends a profile containing credential_process, then triggers model validation so botocore runs the attacker's command as root. Different entry point, same shape: text from a user crosses into a context where it is interpreted as instruction, and the boundary between data and command was never drawn.
That is the pattern running through this year's agent-platform disclosures, from PraisonAI's 27-CVE batch to Kiro's repeat file-write class. Agent frameworks compose capability by default and gate it by exception, and the exceptions are written by whoever remembers what the framework attached.
What to do
- Upgrade to 2.10.5-lts or later — 2.10.6-lts is current, published September 3. Versions at or below 2.10.3-lts are affected per the advisory, which notes the vulnerable path likely predates that tag.
- Set
MAXKB_SANDBOX=1explicitly on source and bare-metal deployments. The code default is 0 and the variable is not inConfig.defaults; unset means the wrapper — including the new parser — is bypassed and commands run directly on the host. - Treat public and embedded assistants with attached tools as internet-exposed command surface. This is the
PR:Npath. If an anonymous widget has any tool, MCP server, skill, or sub-app attached, remove the capability or take the bot down until patched. - Audit which assistants have any capability attached. Tool-free chat does not enter the vulnerable path; a single attached MCP server does. This is the fastest way to scope exposure.
- Assume ingested content is an attack channel, not just chat. The RAG and document-upload path reaches the same
executetool without the attacker ever holding an account. - Run the container as a non-root user. Neither Dockerfile has a
USERdirective. That is what turned a sandbox bypass into a root-owned write, and it is fixable in your own deployment today. - Review the rest of the September batch, not just this CVE. The hardcoded
SECRET_KEY, the brute-forceable reset code, and the Bedrockcredential_processinjection are independently serious and are not all closed by the same release. - If you build on
deepagentsor similar frameworks, enumerate what your backend auto-injects. The general lesson is framework-wide: passing a shell backend can grant a shell tool. Diff the tools your agent actually exposes against the tools you configured.
The most useful thing about this advisory is how ordinary the mistake is. Nobody decided to give an enterprise chatbot a root shell. Someone chose a backend that seemed to describe a sandbox, and the capability came attached; someone else wrote an approval gate listing the tools they had in mind. A CVSS 10.0 sits at the intersection of two reasonable-looking lines of code. Until agent frameworks make capability grants explicit and deny-by-default, that intersection will keep producing perfect scores.
Sources:
- 1Panel-dev/MaxKB — GHSA-f36j-f34j-h3rx, Prompt-injectable agent can lead to command execution (credits Lasso Security)
- NVD — CVE-2026-77521 (CVSS 3.1 10.0, published 21 September 2026)
- MaxKB commit 594f50f — command splitting and sandbox command building in
sandbox_shell.py - MaxKB release v2.10.5-lts
- NVD — CVE-2026-79916, MaxKB AWS Bedrock credential injection leading to root command execution (CVSS 9.1)
- MaxKB source —
apps/application/flow/tools.pyat tag v2.10.3-lts