Skip to content

Commit 2f0ff5f

Browse files
kagura-agentmishushakovclaude
authored
fix(sdk): prevent shell injection in MCP config via proper escaping (#1276)
## Summary Fixes #1154 When creating a sandbox with an `mcp` config, the JSON-serialized config is interpolated directly into a shell command wrapped in single quotes. Since `json.dumps()` / `JSON.stringify()` do not escape single quotes, any MCP config value containing a single quote (e.g., API keys, tokens, URLs) breaks out of shell quoting and allows arbitrary command execution inside the sandbox. ## Changes ### Python SDK (`sandbox_async/main.py`, `sandbox_sync/main.py`) - Use `shlex.quote()` to properly escape the JSON config string (4 locations) - `shlex.quote()` is a stdlib function designed exactly for this purpose ### JS/TS SDK (`sandbox/index.ts`) - Add a `shellQuote()` helper that escapes single quotes using the standard `'\'''` pattern (equivalent to Python's `shlex.quote()`) - Apply it to both MCP config interpolation sites (2 locations) ## Before / After **Before** (vulnerable): ``` mcp-gateway --config '{"servers": {"test": {"envs": {"KEY": "it's a value"}}}}' # ^^ breaks out ``` **After** (safe): ``` mcp-gateway --config '{"servers": {"test": {"envs": {"KEY": "it'\''s a value"}}}}' # ^^^^ properly escaped ``` ## Testing Verified escaping behavior for both Python (`shlex.quote`) and JS (`shellQuote`) with the PoC from the issue — single quotes in config values are properly escaped and no longer allow shell breakout. --------- Co-authored-by: Mish Ushakov <10400064+mishushakov@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7695889 commit 2f0ff5f

5 files changed

Lines changed: 23 additions & 6 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"e2b": patch
3+
"@e2b/python-sdk": patch
4+
---
5+
6+
fix(sdk): prevent shell injection in MCP config by using proper shell escaping (shlex.quote in Python, shellQuote helper in JS/TS)

packages/js-sdk/src/sandbox/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getSignature } from './signature'
2929
import { compareVersions } from 'compare-versions'
3030
import { SandboxError } from '../errors'
3131
import { ENVD_DEBUG_FALLBACK, ENVD_DEFAULT_USER } from '../envd/versions'
32+
import { shellQuote } from '../utils'
3233

3334
/**
3435
* Options for sandbox upload/download URL generation.
@@ -301,7 +302,7 @@ export class Sandbox extends SandboxApi {
301302
if (sandboxOpts?.mcp) {
302303
sandbox.mcpToken = crypto.randomUUID()
303304
const res = await sandbox.commands.run(
304-
`mcp-gateway --config '${JSON.stringify(sandboxOpts?.mcp)}'`,
305+
`mcp-gateway --config ${shellQuote(JSON.stringify(sandboxOpts.mcp))}`,
305306
{
306307
user: 'root',
307308
envs: {
@@ -398,7 +399,7 @@ export class Sandbox extends SandboxApi {
398399
if (sandboxOpts?.mcp) {
399400
sandbox.mcpToken = crypto.randomUUID()
400401
const res = await sandbox.commands.run(
401-
`mcp-gateway --config '${JSON.stringify(sandboxOpts?.mcp)}'`,
402+
`mcp-gateway --config ${shellQuote(JSON.stringify(sandboxOpts.mcp))}`,
402403
{
403404
user: 'root',
404405
envs: {

packages/js-sdk/src/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ export function toBlob(
125125
return new Response(data).blob()
126126
}
127127

128+
/**
129+
* Escape a string for safe inclusion in a single-quoted shell argument.
130+
* Equivalent to Python's shlex.quote().
131+
*/
132+
export function shellQuote(s: string): string {
133+
return "'" + s.replace(/'/g, "'\\''") + "'"
134+
}
135+
128136
/**
129137
* Prepare data for upload as a BodyInit, optionally gzip-compressed.
130138
* When gzip is enabled, compresses the data and returns a Blob.

packages/python-sdk/e2b/sandbox_async/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import json
33
import logging
4+
import shlex
45
import uuid
56
from typing import Dict, List, Optional, Union, overload
67

@@ -235,7 +236,7 @@ async def create(
235236
sandbox._mcp_token = token
236237

237238
res = await sandbox.commands.run(
238-
f"mcp-gateway --config '{json.dumps(mcp)}'",
239+
f"mcp-gateway --config {shlex.quote(json.dumps(mcp))}",
239240
user="root",
240241
envs={"GATEWAY_ACCESS_TOKEN": token},
241242
)
@@ -616,7 +617,7 @@ async def beta_create(
616617
sandbox._mcp_token = token
617618

618619
res = await sandbox.commands.run(
619-
f"mcp-gateway --config '{json.dumps(mcp)}'",
620+
f"mcp-gateway --config {shlex.quote(json.dumps(mcp))}",
620621
user="root",
621622
envs={"GATEWAY_ACCESS_TOKEN": token},
622623
)

packages/python-sdk/e2b/sandbox_sync/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import datetime
22
import json
33
import logging
4+
import shlex
45
import uuid
56
from typing import Dict, List, Optional, Union, overload
67

@@ -233,7 +234,7 @@ def create(
233234
sandbox._mcp_token = token
234235

235236
res = sandbox.commands.run(
236-
f"mcp-gateway --config '{json.dumps(mcp)}'",
237+
f"mcp-gateway --config {shlex.quote(json.dumps(mcp))}",
237238
user="root",
238239
envs={"GATEWAY_ACCESS_TOKEN": token},
239240
)
@@ -617,7 +618,7 @@ def beta_create(
617618
sandbox._mcp_token = token
618619

619620
res = sandbox.commands.run(
620-
f"mcp-gateway --config '{json.dumps(mcp)}'",
621+
f"mcp-gateway --config {shlex.quote(json.dumps(mcp))}",
621622
user="root",
622623
envs={"GATEWAY_ACCESS_TOKEN": token},
623624
)

0 commit comments

Comments
 (0)