# Tool arguments are validated against inputSchema before execution

- **Pattern:** `ab-001847` (`mcp-server.security-capabilities.input-validation`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001847
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001847)

## Why it matters

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.

## Severity rationale

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.

## Remediation

Use Zod schemas in the SDK — validation happens automatically before the handler runs. Add security-specific refinements for file paths and sensitive inputs.

```ts
// 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()`.

## Detection

- **ID:** `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:

  ```ts
  // 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) })
  ```

  ```ts
  // 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}` }] }
  })
  ```

## External references

- cwe CWE-20
- cwe CWE-78
- owasp A03
- owasp-llm LLM01
- external mcp-spec-tool-inputschema — https://modelcontextprotocol.io/docs/concepts/tools#tool-definition
- cwe CWE-22

Taxons: injection-and-input-trust, inference-contract

HTML version: https://auditbuffet.com/patterns/ab-001847
