Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions typescript/mcp/security/mcp-command-injection-typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { exec, execSync, execFile } from "child_process";
import * as cp from "child_process";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "test-server", version: "1.0.0" });

server.setRequestHandler("tools/call", async (REQ) => {
const cmd = REQ.params.arguments.command;
// ruleid: mcp-command-injection-typescript
exec(cmd);
return {};
});

server.tool("run-command", { command: z.string() }, async (args) => {
// ruleid: mcp-command-injection-typescript
exec(args.command, () => {});
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("run-sync", { command: z.string() }, async ({ command }) => {
// ruleid: mcp-command-injection-typescript
execSync(command);
return { content: [{ type: "text" as const, text: "ok" }] };
});

server.registerTool(
"run-v2",
{ description: "run", inputSchema: z.object({ command: z.string() }) },
async ({ command }) => {
// ruleid: mcp-command-injection-typescript
cp.execSync(command);
return { content: [{ type: "text" as const, text: "done" }] };
}
);

server.tool("safe-zod", { command: z.string() }, async ({ command }) => {
const safe = z.string().parse(command);
// ok: mcp-command-injection-typescript
exec(safe);
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("list-files", {}, async () => {
// ok: mcp-command-injection-typescript
exec("ls -la /tmp");
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("safe-execfile", { filename: z.string() }, async ({ filename }) => {
// ok: mcp-command-injection-typescript
execFile("cat", [filename]);
return { content: [{ type: "text" as const, text: "done" }] };
});

function unrelated(cmd: string) {
// ok: mcp-command-injection-typescript
exec(cmd);
}
81 changes: 81 additions & 0 deletions typescript/mcp/security/mcp-command-injection-typescript.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
rules:
- id: mcp-command-injection-typescript
mode: taint
languages: [typescript, javascript]
severity: ERROR
message: >-
User-controlled MCP tool or request-handler argument flows to a shell
execution sink without validation. Tool arguments originate from LLM or
client input. Use execFile() with an explicit argument array, or validate
with zod.parse() / schema.parse() before calling exec() or execSync().
metadata:
cwe:
- "CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')"
category: security
confidence: HIGH
subcategory:
- vuln
likelihood: MEDIUM
impact: HIGH
technology:
- mcp
- typescript
references:
- https://modelcontextprotocol.io/specification/draft/basic/security_best_practices
pattern-sources:
- patterns:
- pattern-inside: |
$SRV.setRequestHandler(..., async ($REQ) => {
...
})
- pattern: $REQ.params.arguments.$ARG
- patterns:
- pattern-inside: |
$SRV.setRequestHandler(..., ($REQ) => {
...
})
- pattern: $REQ.params.arguments.$ARG
- patterns:
- pattern-inside: |
$SRV.tool(..., async ($ARGS) => {
...
})
- pattern: $ARGS
- patterns:
- pattern-inside: |
$SRV.tool(..., async ({ $FIELD }) => {
...
})
- pattern: $FIELD
- patterns:
- pattern-inside: |
$SRV.registerTool(..., async ($ARGS) => {
...
})
- pattern: $ARGS
- patterns:
- pattern-inside: |
$SRV.registerTool(..., async ({ $FIELD }) => {
...
})
- pattern: $FIELD
pattern-sinks:
- patterns:
- pattern: exec($CMD, ...)
- focus-metavariable: $CMD
- patterns:
- pattern: execSync($CMD, ...)
- focus-metavariable: $CMD
- patterns:
- pattern: $CP.exec($CMD, ...)
- focus-metavariable: $CMD
- patterns:
- pattern: $CP.execSync($CMD, ...)
- focus-metavariable: $CMD
- patterns:
- pattern: eval($CMD)
- focus-metavariable: $CMD
pattern-sanitizers:
- pattern: z.parse(...)
- pattern: zod.parse(...)
- pattern: $SCHEMA.parse(...)
107 changes: 107 additions & 0 deletions typescript/mcp/security/mcp-security.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { exec, execSync, execFile } from "child_process";
import * as cp from "child_process";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import axios from "axios";

const server = new McpServer({ name: "test-server", version: "1.0.0" });
const urlSchema = z.string().url();

// --- setRequestHandler nested args (command injection TP) ---

server.setRequestHandler("tools/call", async (REQ) => {
const cmd = REQ.params.arguments.command;
// ruleid: mcp-command-injection-typescript
exec(cmd);
return {};
});

// --- server.tool TP ---

server.tool("run-command", { command: z.string() }, async (args) => {
// ruleid: mcp-command-injection-typescript
exec(args.command, () => {});
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("run-sync", { command: z.string() }, async ({ command }) => {
// ruleid: mcp-command-injection-typescript
execSync(command);
return { content: [{ type: "text" as const, text: "ok" }] };
});

server.registerTool(
"run-v2",
{ description: "run", inputSchema: z.object({ command: z.string() }) },
async ({ command }) => {
// ruleid: mcp-command-injection-typescript
cp.execSync(command);
return { content: [{ type: "text" as const, text: "done" }] };
}
);

// --- command injection TN (zod.parse sanitizer) ---

server.tool("safe-zod", { command: z.string() }, async ({ command }) => {
const safe = z.string().parse(command);
// ok: mcp-command-injection-typescript
exec(safe);
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("list-files", {}, async () => {
// ok: mcp-command-injection-typescript
exec("ls -la /tmp");
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("safe-execfile", { filename: z.string() }, async ({ filename }) => {
// ok: mcp-command-injection-typescript
execFile("cat", [filename]);
return { content: [{ type: "text" as const, text: "done" }] };
});

// --- SSRF TP ---

server.setRequestHandler("resources/read", async (REQ) => {
const url = REQ.params.arguments.url;
// ruleid: mcp-ssrf-typescript
await fetch(url);
return {};
});

server.tool("fetch-url", { url: z.string() }, async ({ url }) => {
// ruleid: mcp-ssrf-typescript
await axios.get(url);
return { content: [{ type: "text" as const, text: "done" }] };
});

server.registerTool(
"proxy-v2",
{ description: "proxy", inputSchema: z.object({ url: z.string() }) },
async ({ url }) => {
// ruleid: mcp-ssrf-typescript
await fetch(url);
return { content: [{ type: "text" as const, text: "done" }] };
}
);

// --- SSRF TN (zod.parse / schema.parse) ---

server.tool("safe-fetch", { url: z.string() }, async ({ url }) => {
const safeUrl = urlSchema.parse(url);
// ok: mcp-ssrf-typescript
await fetch(safeUrl);
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("hardcoded-fetch", {}, async () => {
// ok: mcp-ssrf-typescript
await fetch("https://example.com/api");
return { content: [{ type: "text" as const, text: "done" }] };
});

function unrelated(cmd: string) {
// ok: mcp-command-injection-typescript
exec(cmd);
}
42 changes: 42 additions & 0 deletions typescript/mcp/security/mcp-ssrf-typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import axios from "axios";

const server = new McpServer({ name: "test-server", version: "1.0.0" });
const urlSchema = z.string().url();

server.setRequestHandler("resources/read", async (REQ) => {
const url = REQ.params.arguments.url;
// ruleid: mcp-ssrf-typescript
await fetch(url);
return {};
});

server.tool("fetch-url", { url: z.string() }, async ({ url }) => {
// ruleid: mcp-ssrf-typescript
await axios.get(url);
return { content: [{ type: "text" as const, text: "done" }] };
});

server.registerTool(
"proxy-v2",
{ description: "proxy", inputSchema: z.object({ url: z.string() }) },
async ({ url }) => {
// ruleid: mcp-ssrf-typescript
await fetch(url);
return { content: [{ type: "text" as const, text: "done" }] };
}
);

server.tool("safe-fetch", { url: z.string() }, async ({ url }) => {
const safeUrl = urlSchema.parse(url);
// ok: mcp-ssrf-typescript
await fetch(safeUrl);
return { content: [{ type: "text" as const, text: "done" }] };
});

server.tool("hardcoded-fetch", {}, async () => {
// ok: mcp-ssrf-typescript
await fetch("https://example.com/api");
return { content: [{ type: "text" as const, text: "done" }] };
});
84 changes: 84 additions & 0 deletions typescript/mcp/security/mcp-ssrf-typescript.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
rules:
- id: mcp-ssrf-typescript
mode: taint
languages: [typescript, javascript]
severity: ERROR
message: >-
User-controlled MCP tool or request-handler argument flows to an HTTP
request URL without validation. An attacker may supply internal network
URLs (SSRF). Validate URLs with zod.parse() or an allowlist before
calling fetch() or axios.
metadata:
cwe:
- "CWE-918: Server-Side Request Forgery (SSRF)"
category: security
confidence: HIGH
subcategory:
- vuln
likelihood: MEDIUM
impact: HIGH
technology:
- mcp
- typescript
references:
- https://modelcontextprotocol.io/specification/draft/basic/security_best_practices
pattern-sources:
- patterns:
- pattern-inside: |
$SRV.setRequestHandler(..., async ($REQ) => {
...
})
- pattern: $REQ.params.arguments.$ARG
- patterns:
- pattern-inside: |
$SRV.setRequestHandler(..., ($REQ) => {
...
})
- pattern: $REQ.params.arguments.$ARG
- patterns:
- pattern-inside: |
$SRV.tool(..., async ($ARGS) => {
...
})
- pattern: $ARGS
- patterns:
- pattern-inside: |
$SRV.tool(..., async ({ $FIELD }) => {
...
})
- pattern: $FIELD
- patterns:
- pattern-inside: |
$SRV.registerTool(..., async ($ARGS) => {
...
})
- pattern: $ARGS
- patterns:
- pattern-inside: |
$SRV.registerTool(..., async ({ $FIELD }) => {
...
})
- pattern: $FIELD
pattern-sinks:
- patterns:
- pattern: fetch($URL, ...)
- focus-metavariable: $URL
- patterns:
- pattern: axios.get($URL, ...)
- focus-metavariable: $URL
- patterns:
- pattern: axios.post($URL, ...)
- focus-metavariable: $URL
- patterns:
- pattern: axios.put($URL, ...)
- focus-metavariable: $URL
- patterns:
- pattern: axios.delete($URL, ...)
- focus-metavariable: $URL
- patterns:
- pattern: axios($URL, ...)
- focus-metavariable: $URL
pattern-sanitizers:
- pattern: z.parse(...)
- pattern: zod.parse(...)
- pattern: $SCHEMA.parse(...)