Security1 distinct publisher2 min readPublished
The POST /mcp endpoint took a command string from any client that could open a session and checked only the program name against an allowlist. Chainlit 2.12.0 stops accepting the command at all.
The Watch · Security desk

Compiled by The WatchSomething wrong?How this is made
The pivot is npx. shlex.split() parses the command string, validate_mcp_command() confirms the first token is on the allowlist, and returns the arguments untouched to a subprocess spawner [2]. npx accepts -c, which runs an arbitrary shell string, so `npx -y -c 'PAYLOAD'` satisfies a check that only ever looked at the word npx [3]. The allowlist did exactly what it was written to do. It guarded the wrong token.
There is a second way in. allowed_executables defaults to None, and the validation code reads None as allow-everything. A developer who deletes that line while tidying the config opens the gate to any binary, not just wrappers like npx or uvx [4].
Reaching the sink takes one unauthenticated step. The /mcp route is registered on the FastAPI router in every build; the only gates are the runtime features.mcp.enabled flag and, where someone configured it, auth on /mcp [5]. The proof of concept opens a Socket.IO session for a sessionId, posts a fullCommand, and the server runs it before the MCP handshake even fails, writing the output of `id` to /tmp/rce_proof [6]. MCP has shipped disabled by default since 2.7.0, which is the main reason this is not everywhere [7].
The fix says which control mattered. Version 2.12.0 does not add stricter argument parsing. It removes fullCommand from the client request entirely; stdio servers are declared by the developer in config.toml and picked by name, so the command never crosses from client to server and there is nothing left to sanitize [8]. The reliable rule for anyone shipping MCP stdio servers is narrower than "validate arguments": do not take the command from the caller.
Two caveats. The upgrade breaks configs. Legacy [features.mcp.stdio] sections and the allowed_executables setting will fail to start once MCP is enabled, until they are migrated to [[features.mcp.servers]] [9]. And the same release closes a companion SSRF tracked as CVE-2026-45019; setting features.mcp.enabled = false prevents exploitation of both [10].
Ranked by verification strength, evidence, and original report placement.
Chainlit advisory GHSA-w3fx-mc44-mf6j reports command injection via the MCP stdio transport that allows unauthenticated remote code execution, running OS commands with the privileges of the Chainlit process.
validate_mcp_command() in backend/chainlit/mcp.py uses shlex.split() and validates only the executable name against config.features.mcp.stdio.allowed_executables; the arguments are returned unchecked and passed to StdioServerParameters, which spawns a subprocess.
Because npx supports -c for arbitrary shell execution, the payload npx -y -c 'PAYLOAD' passes the executable allowlist check while running whatever the attacker specifies.
allowed_executables defaults to None in the Pydantic model, and the validation code treats None as allow-everything; if a developer removes the allowed_executables line from config, any executable can be invoked.
The /mcp route is registered unconditionally on the FastAPI router in every Chainlit deployment; only the runtime features.mcp.enabled check and, where configured, authentication on /mcp prevent exploitation, and no authentication is required by default so any client that can open a session can reach it.
A proof of concept tested against Chainlit 2.11.0 with features.mcp.enabled = true and default settings opens a Socket.IO session to obtain a sessionId, posts a fullCommand to /mcp, and the server runs the command before the MCP handshake fails, writing the output of id to /tmp/rce_proof.
Distinct publishers with included, body-backed reporting in this cluster.
Follow any of these and your For You feed starts watching them — no settings page required.
build
Semantic code search over a monorepo is now a plumbing job, and the plumbing is the hard part1 distinct publisher
build
255 tool schemas, 91K tokens: pricing the two MCP costs nobody budgets1 distinct publisher
build
Return the admission record, not the log line: one memory service's case for receipts1 distinct publisher
build
Queue arithmetic explains the 47-second wait in a collapsed LLM prototype1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-party and checkable
Chainlit names the function, the file and the sink, and the exploit is a single npx flag rather than an exotic chain. The proof of concept pins 2.11.0 and ends with a concrete artefact on disk, which is about as falsifiable as an advisory gets. Two gaps keep this short of the top: nobody outside the project has reproduced it, and the text never states which release first accepted a client-supplied fullCommand, so the affected range has no lower bound in what we can read.
Exposure never counted
Only one event is visible: 2.12.0 shipped. How many Chainlit deployments carry features.mcp.enabled = true, and how many of those have upgraded or turned MCP off, appears nowhere in this reporting. A shipped patch is not an uptake measurement, and Chainlit offers no probe or exploitation data either.
Quietly understated
The advisory rates the flaw Critical and then immediately narrows its own blast radius, which is the opposite of inflation. Where it undersells itself is in the detail: allowed_executables defaults to None and the code reads None as permit-everything, meaning the control the headline treats as bypassed was optional to begin with. The residual finding, that an anonymous client can still trigger developer-declared process spawning on a patched deployment without auth, sits below the fix rather than beside it.
The vendor writes the record
Chainlit is both the party with the bug and the party assessing it, and the framing follows that position. The first line of the advisory limits who is affected, the fix section presents a redesign as the answer, and the breaking config migration is disclosed but placed after the remedy. Pulling the other way, the technical work is credited to Vipin and SPL, who had no reason to soften it, and the level of code detail is consistent with an outside report published largely intact.
Mechanism solid, scope unverified
Two different confidences are bundled in one document. The exploit path is specific enough to test and comes with a reproduction, so we can lean on it. The scoping, meaning how many installs enable MCP and whether anyone has been hit, rests entirely on the maintainers' judgement with no second source to check it against.