Tool arguments come from AI model output, which can be manipulated via prompt injection (OWASP LLM01). An MCP tool that executes a shell command, writes a file, or queries a database using arguments it hasn't validated is wide open to CWE-78 (OS command injection) and CWE-22 (path traversal). Even without a malicious actor, an AI model generating plausible-but-wrong parameter values (wrong types, out-of-range numbers) will hit unvalidated code paths and produce crashes or corrupted state. Input schema validation is the gating control before any side effect executes.
Critical because unvalidated tool inputs enable injection attacks (CWE-78, CWE-22) and prompt injection exploitation (OWASP LLM01) that can execute arbitrary commands or access unauthorized files.
Use Zod schemas in the SDK — validation happens automatically before the handler runs. Add security-specific refinements for file paths and sensitive inputs.
// src/tools/files.ts — validated file write
server.tool('write_file', 'Write content to a file within the project', {
path: z.string()
.refine(p => !p.includes('..'), 'Path traversal not allowed')
.refine(p => p.startsWith('/workspace/'), 'Must be within /workspace'),
content: z.string().max(1_000_000, 'Content exceeds 1MB limit'),
}, async ({ path, content }) => {
// path and content are validated before this line executes
await fs.writeFile(path, content)
return { content: [{ type: 'text', text: `Wrote ${content.length} bytes` }] }
})
For Python, use Pydantic model validation on the function arguments decorated with @mcp.tool().
ID: mcp-server.security-capabilities.input-validation
Severity: critical
What to look for: Count all tool handlers that accept user input. Enumerate which validate input against their JSON Schema vs. which trust input blindly. Check whether tool handlers validate their input arguments against the declared inputSchema before executing. In SDK-based servers using Zod schemas, the SDK validates automatically — verify this isn't bypassed. For custom implementations, check for explicit validation of argument types, required fields, and value ranges. Check for tools that accept file paths without validating them, SQL without parameterization, or shell commands without sanitization.
Pass criteria: All tool inputs are validated against their declared schemas before the handler executes. Type mismatches, missing required fields, and out-of-range values are caught and returned as errors before any side effects occur. 100% of tools accepting user input must validate against their declared JSON Schema before processing.
Fail criteria: Tool handlers directly use arguments without validation, or validation is incomplete (checks types but not required fields), or schema validation is bypassed.
Skip (N/A) when: The server registers no tools. All checks skip when no MCP server is detected.
Report even on pass: Report validation coverage: "X of Y tools validate input against JSON Schema."
Cross-reference: For input schema definitions, see input-schemas. For malformed input handling, see malformed-input.
Detail on fail: "Tool 'run_command' accepts a 'command' string with no validation or sanitization — any shell command can be executed" or "Tool 'write_file' does not validate 'path' parameter — can write to any location on the filesystem including system directories"
Remediation: Use Zod schemas (TypeScript) or Pydantic (Python) for automatic validation:
// src/tools/search.ts — validate input with zod
import { z } from 'zod'
const SearchInput = z.object({ query: z.string().min(1), limit: z.number().max(100).default(10) })
// Zod validates automatically when used with the SDK
server.tool('write_file', 'Write content to a file within the project directory', {
path: z.string()
.refine(p => !p.includes('..'), 'Path traversal not allowed')
.refine(p => p.startsWith('/allowed/dir/'), 'Must be within project directory'),
content: z.string().max(1_000_000, 'Content too large'),
}, async ({ path, content }) => {
// path and content are already validated by Zod
await fs.writeFile(path, content)
return { content: [{ type: 'text', text: `Wrote ${content.length} bytes to ${path}` }] }
})