Skip to content

security(mcp): sanitize MCP tool definitions at registration to prevent tool-poisoning injection#1745

Merged
bug-ops merged 1 commit intomainfrom
security-sanitize-mcp-tool-de
Mar 14, 2026
Merged

security(mcp): sanitize MCP tool definitions at registration to prevent tool-poisoning injection#1745
bug-ops merged 1 commit intomainfrom
security-sanitize-mcp-tool-de

Conversation

@bug-ops
Copy link
Copy Markdown
Owner

@bug-ops bug-ops commented Mar 14, 2026

Summary

Closes #1691.

Malicious MCP servers can embed prompt injection payloads in tool description and inputSchema fields. These bypass ContentSanitizer because they arrive as trusted system content (tool catalog), not as user or web content.

New crates/zeph-mcp/src/sanitize module applies sanitization at registration time before any tool definition reaches the LLM context:

  • 17 injection-detection regexes (LazyLock, compiled once): system-prompt override, role injection, jailbreak phrases, data exfiltration, URL execution, XML/HTML tag escape, base64 blobs, shell commands, delimiter escapes — all with (?i) case-insensitive matching
  • Unicode Cf-category strip pass before regex matching — defeats zero-width and format-character bypass attempts (U+200B–U+200F, U+202A–U+202F, U+FEFF, Tags block)
  • Whole-field replacement with "[sanitized]" on any match — no surgical replacements that preserve surrounding attacker-controlled text
  • Structured WARN log on detection (server_id, tool_name, field, truncated matched text with control chars stripped)
  • sanitize_tool_name: restricts to [a-zA-Z0-9_-], max 64 chars, fallback "_unnamed" — prevents XML attribute injection in prompt.rs
  • sanitize_server_id: restricts to [a-zA-Z0-9_.-], max 128 chars, fallback "_unnamed" — same defense for server_id XML interpolation
  • Recursive JSON schema walker: sanitizes ALL string values (title, enum, default, examples, const, description), depth-capped at 10
  • 1024-byte description cap at char boundary

Hook: sanitize_tools() called in both McpManager::connect_all() and add_server() immediately after list_tools() returns. Tool registration is never blocked — only text is cleaned.

Test plan

  • cargo nextest run -p zeph-mcp --features full — 212 tests, all pass (+23 new)
  • cargo clippy --workspace --features full -- -D warnings — 0 warnings
  • cargo +nightly fmt --check — clean
  • Full workspace: 5487 tests pass

Follow-up issues

  • Injection pattern drift between zeph-mcp and zeph-core sanitizers (extract to shared module)
  • tools/list_changed MCP notification path — refreshed tools bypass sanitization
  • JSON schema node count cap (current: unbounded width, depth-capped at 10)

…ool-poisoning injection (#1691)

Malicious MCP servers can embed prompt injection payloads in tool `description`
and `inputSchema` fields. These bypass ContentSanitizer because they arrive as
trusted system content (tool catalog), not as user or web content.

New `crates/zeph-mcp/src/sanitize` module:
- 17 injection-detection regexes compiled once via LazyLock (system-prompt
  override, role injection, jailbreak, data exfiltration, URL execution,
  XML/HTML tag escape, base64 blobs, shell commands, delimiter escapes)
- Unicode Cf-category strip pass before regex matching to defeat zero-width
  and format-character bypass attempts
- Whole-field replacement with "[sanitized]" on any pattern match
- Structured WARN log on detection (server_id, tool_name, field, matched text)
- `sanitize_tool_name`: restricts to [a-zA-Z0-9_-], fallback "_unnamed"
- `sanitize_server_id`: restricts to [a-zA-Z0-9_.-], fallback "_unnamed"
- Recursive JSON schema walker sanitizes ALL string values (title, enum,
  default, examples, const, description), depth-capped at 10
- Descriptions capped at 1024 bytes (char boundary safe)

Hook: `sanitize_tools()` called in `McpManager::connect_all()` and
`add_server()` immediately after `list_tools()` returns. Tool registration
is never blocked — only text is cleaned.

+23 unit tests (zeph-mcp: 189 → 212).
@bug-ops bug-ops added the security Security-related issue label Mar 14, 2026
@github-actions github-actions bot added size/XL Extra large PR (500+ lines) documentation Improvements or additions to documentation rust Rust code changes dependencies Dependency updates labels Mar 14, 2026
@bug-ops bug-ops enabled auto-merge (squash) March 14, 2026 13:51
@bug-ops bug-ops merged commit 9efbb9d into main Mar 14, 2026
15 checks passed
@bug-ops bug-ops deleted the security-sanitize-mcp-tool-de branch March 14, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Dependency updates documentation Improvements or additions to documentation rust Rust code changes security Security-related issue size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: sanitize MCP tool descriptions at registration to prevent tool-poisoning injection (Log-To-Leak)

1 participant