diff --git a/typescript/mcp/security/mcp-command-injection-typescript.ts b/typescript/mcp/security/mcp-command-injection-typescript.ts new file mode 100644 index 0000000000..0b376006c1 --- /dev/null +++ b/typescript/mcp/security/mcp-command-injection-typescript.ts @@ -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); +} diff --git a/typescript/mcp/security/mcp-command-injection-typescript.yaml b/typescript/mcp/security/mcp-command-injection-typescript.yaml new file mode 100644 index 0000000000..217684f388 --- /dev/null +++ b/typescript/mcp/security/mcp-command-injection-typescript.yaml @@ -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(...) diff --git a/typescript/mcp/security/mcp-security.ts b/typescript/mcp/security/mcp-security.ts new file mode 100644 index 0000000000..2d25cb490a --- /dev/null +++ b/typescript/mcp/security/mcp-security.ts @@ -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); +} diff --git a/typescript/mcp/security/mcp-ssrf-typescript.ts b/typescript/mcp/security/mcp-ssrf-typescript.ts new file mode 100644 index 0000000000..9b59a9a39d --- /dev/null +++ b/typescript/mcp/security/mcp-ssrf-typescript.ts @@ -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" }] }; +}); diff --git a/typescript/mcp/security/mcp-ssrf-typescript.yaml b/typescript/mcp/security/mcp-ssrf-typescript.yaml new file mode 100644 index 0000000000..26b0ac6597 --- /dev/null +++ b/typescript/mcp/security/mcp-ssrf-typescript.yaml @@ -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(...)