MCP Tool Schemas Are Untrusted: Validators Must Enforce Limits Against SSRF and DoS

The MCP 2026-07-28 spec relaxes tool schemas but adds constraints on external references and combinatorial keywords; testing shows jsonschema defaults to fetching remote refs (violating spec) while Ajv complies, and a tiny 1.3KB schema with nested anyOf can cause 10-second validation delays, so validators must enforce time budgets and reject external refs.

Architecture Development Notes
Architecture Development Notes
Architecture Development Notes
MCP Tool Schemas Are Untrusted: Validators Must Enforce Limits Against SSRF and DoS

Spec Changes in MCP 2026-07-28

The MCP 2026-07-28 specification (SEP-2106) relaxes inputSchema and outputSchema to allow any JSON Schema 2020-12 keywords (top-level must still be type: "object"), and structuredContent can be any JSON value. However, the schemas used to validate model parameters and tool results are supplied by the server via tools/list, making them untrusted input.

The spec adds two new constraints:

External references: Implementations MUST NOT automatically dereference refs pointing to network URIs. An explicit opt-in mode may be provided but MUST be disabled by default, SHOULD restrict to a host allowlist (at minimum rejecting loopback, link-local, and private addresses), enforce timeouts and size limits, and log fetched URIs. If validation fails due to an unresolvable external ref, the implementation SHOULD reject the schema rather than silently allowing it. The spec names this risk SSRF and Fetch-DoS (fetch amplification): a malicious tool definition can make the host access internal metadata endpoints or download large resource-exhausting files.

Combinatorial keywords: anyOf, oneOf, allOf, if/then/else, and $defs increase expressive power but can impose high validation cost. Implementations SHOULD set reasonable limits such as maximum schema depth, total sub-schema count, or per-validation time budget to prevent malicious schemas from becoming denial-of-service vectors against the validator. The spec gives these as examples ("e.g.") without mandating which to use or how to measure depth/count.

The tool-page security section adds that clients SHOULD follow the above ref rules when validating inputs/outputs with inputSchema and outputSchema. These constraints did not exist in the 2025-11-25 version.

Test 1: Default Behavior on Remote Refs

Environment: macOS, Python 3.11.15 with jsonschema 4.26.0, Node 22.23.3 with Ajv 8.20.0. A test schema referenced a local HTTP server at http://127.0.0.1:18999/defs.json (returning {"type": "integer"}).

{"type": "object", "properties": {"q": {"$ref": "http://127.0.0.1:18999/defs.json"}}}

jsonschema 4.26.0: Draft202012Validator(schema) with no extra configuration performs a GET request to the remote URI. Validating three times with the same validator object sends three requests. The library emits a DeprecationWarning stating that automatic remote ref retrieval poses security hazards, is discouraged by the JSON Schema spec, and will soon become an error. Thus the default behavior violates the spec's MUST NOT, though the library authors are aware and moving toward compliance. The fetched content directly affects validation outcome: {"q": "abc"} fails, {"q": 3} passes — meaning the server can both induce network requests and alter validation logic.

Ajv 8.20.0: Synchronous compile throws "can't resolve reference" and makes no network request. Only when an explicit loadSchema callback is provided and compileAsync is used does it fetch the remote schema — matching the spec's allowed opt-in mode. When jsonschema was pointed at an unreachable address, it threw an exception and did not allow validation, satisfying the "reject rather than silently allow" requirement.

These results reflect only the default behavior of these two library versions; the author did not verify which validators MCP SDKs actually use, nor test against cloud metadata endpoints.

Test 2: Combinatorial Explosion with a 1.3 KB Schema

The second constraint (SHOULD set limits) is harder to satisfy because the spec does not define how to measure "depth". The author constructed a minimal schema using $defs with entries n0 through n18 (19 entries). Each entry is an anyOf with two branches, both referencing the next entry; the final entry requires a string with minLength: 1000000.

def build(d):
    defs = {}
    for i in range(d):
        defs[f"n{i}"] = {"anyOf": [{"$ref": f"#/$defs/n{i+1}"}, {"$ref": f"#/$defs/n{i+1}"}]}
    defs[f"n{d}"] = {"type": "string", "minLength": 1000000}
    return {"type": "object", "$defs": defs, "properties": {"q": {"$ref": "#/$defs/n0"}}}

Validating instance {"q": 1} (a number, not a string) forces the validator to try every branch at every leaf because each anyOf fails. Single-run timings (schema size in parentheses):

Depth 10: 795 bytes, 0.016 s

Depth 14: 1075 bytes, 0.321 s

Depth 16: 1215 bytes, 2.533 s

Depth 18: 1355 bytes, 10.411 s

Size grew ~70% while time increased ~650x. Theoretical leaf count doubles per level, consistent with the trend, though single-run multipliers are not exact. No Ajv test was performed for this case.

This example shows that simple limits like maximum document-level nesting depth or sub-schema count (without expanding refs) may not catch such schemas. A per-validation time budget is the only limit that works without relying on expansion rules. If depth is measured after ref expansion, a depth limit could also catch it, but the spec does not specify this.

Where to Place the Validation Layer

Conclusion: treat schemas from tools/list as inputs that must be inspected and compiled with safeguards, not as trusted configuration.

Static inspection on arrival: Scan all $ref values; reject any that do not start with # (corresponding to the spec's "reject this schema" option). Minimal implementation:

def external_refs(node, found=None):
    found = [] if found is None else found
    if isinstance(node, dict):
        ref = node.get("$ref")
        if isinstance(ref, str) and not ref.startswith("#"):
            found.append(ref)
        for v in node.values():
            external_refs(v, found)
    elif isinstance(node, list):
        for v in node:
            external_refs(v, found)
    return found

This scan covers only $ref; JSON Schema 2020-12 also has $id, dynamic refs, etc. It should be a first filter, not a complete defense.

Time budget on the validation call: A synchronous validation that blocks for 10 seconds stalls the entire agent turn. Run validation in a subprocess or worker with a timeout; on timeout, discard the tool. Both parameter validation (pre-call) and result validation (post-return) need this protection.

Unified failure handling: Scan rejection, compilation error, and timeout should all make the tool unavailable and log the reason — never fall back to skipping validation. The spec requires rejecting schemas with unresolved external refs rather than allowing them; timeouts and exceptions should be treated the same way.

Trust level does not remove the need for these guards: Even for internally deployed, audited servers, tools/list content can change at any time. A fixed pre-check is simpler than per-server trust policies.

Unverified Areas

Only jsonschema 4.26.0 and Ajv 8.20.0 were tested for remote-ref defaults, and only jsonschema for the combinatorial explosion. How individual MCP SDKs implement the 2026-07-28 constraints after release has not been audited. The external-ref MUST NOT applies to all implementations; the tool-page requirement for clients is SHOULD; combinatorial limits are also SHOULD. Real-world implementations may vary widely. If your client or gateway chooses its own validator, replicate the two experiments on your stack.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JSON SchemaMCPvalidationsecuritybenchmarkjsonschemaSSRFDoSAjv
Architecture Development Notes
Written by

Architecture Development Notes

Focused on architecture design, technology trend analysis, and practical development experience sharing.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.