Skip to content

Security: limit tool-call input elements, require MCP SDK 1.32.0, ip-address 10.7.4 - #896

Merged
irparent merged 1 commit into
mainfrom
fix/sdk-input-limits
Oct 22, 2026
Merged

irparent merged 1 commit into
mainfrom
fix/sdk-input-limits

Conversation

@irparent

Copy link
Copy Markdown
Member

Three dependency-security changes in one pull request: a limit on how many array elements and object keys a tool call's arguments may hold, a higher floor for the MCP SDK, and a patched ip-address.

What changes

1. A tool call's arguments may hold at most 100,000 array elements and object keys, on every transport.
GHSA-46jr-4wx8-v453 (high): the SDK validated every element of a tool call's arguments with no limit on how many, and its fix is an McpServer option, maxToolInputElements, that is off by default. Upgrading the SDK alone changes nothing. Iris now sets it on every MCP server it builds: the one stdio serves and the one each HTTP session gets (src/server.ts). The SDK counts the elements before it validates, so a call over the limit gets an isError result naming the limit, never reaches the tool, and the session goes on.

  • New setting security.maxToolInputElements in config.json, or IRIS_MAX_TOOL_INPUT_ELEMENTS: a whole number of at least 1, default 100000. Anything else refuses startup, naming the setting.
  • Documented in --help, the README's environment-variable table and security list, server.json, and docs/api-reference.md.
  • A config object built without the key (an embedder calling createIrisServer directly) still gets the default.

2. @modelcontextprotocol/sdk floor ^1.30.0 → ^1.32.0.
A published package does not carry Iris's lockfile, so a project that already held SDK 1.30.x or 1.31.x kept it when it installed Iris. Those versions are affected by GHSA-46jr-4wx8-v453, GHSA-22jm-h49p-29qw, GHSA-rvq5-wwqv-78pq and GHSA-6prh-2h8m-c8cw; 1.30.x also by GHSA-6qxp-vccf-f47h, and 1.30.0 by GHSA-f897-62hf-6gxq. An SDK older than 1.32.0 also ignores the option in part 1. The lockfile already installed 1.32.0 and stays there (1.32.1 changes documentation only).

3. ip-address 10.7.2 → 10.7.4, overrides floor ^10.5.1 → ^10.7.4.
GHSA-rxp7-6hxx-x8q5 (moderate): Address6 strips a zone identifier in quadratic time before its length limit. Iris reaches ip-address only through express-rate-limit, which parses req.ip. Iris leaves Express's trust proxy off, so req.ip is the socket's own address and a client cannot hand it a long string. Recorded in SECURITY-EXPOSURE.md with the SDK change.

Why 100,000

The bytes were already bounded: security.requestSizeLimit (1 MB) applies to HTTP and stdio alike. The element count was not, and it is what costs memory. Arguments that fail validation are reported element by element, at 63 to 148 characters of error text per element, so the reply to a small request can be many times its size.

All counts below use the SDK's own counting (array elements plus object keys, as in toolInputElementCount in @modelcontextprotocol/sdk/dist/esm/server/mcp.js).

Legitimate inputs. These set how high the limit must be.

Input Elements
Largest of the 929 tool calls the default test suite sends through the SDK (396 test files, in-process and spawned servers) 39
Largest of 1,590 legitimate inputs stored in the repository (1,548 proof-corpus cases, 24 real agent transcripts, and 18 OpenTelemetry exports and other fixtures): a 201-call trace in proof/corpus/max_steps.json 1,009
The same shapes scaled up to fill the whole 1 MB request:
· tool calls from the real agent transcripts 12,615
· OpenTelemetry spans exported by LangGraph, LlamaIndex, OpenAI Agents 11,907 to 14,889
· Iris's own tools/list result passed as tools 34,474
· the 201-call trace, the densest shape found 68,506

What an attacker can send. A 1 MB request carries at most 524,229 elements (an array of zeros). One such request cost the server 260 MB.

Twenty requests at once, the MCP endpoint's default per-minute limit for one client: server peak RSS above its baseline after garbage collection, and the time until all 20 were answered. The payload is the worst one found under each limit. A call over the limit is refused before validation, so the worst case sits just under it.

Limit spans of zeros spans of empty objects
none (before this change) 871 MB, 9 s 1,148 MB, 41 s
10,000 164 MB, 0.3 s 255 MB, 1.0 s
50,000 371 MB, 1.3 s 533 MB, 8.6 s
100,000 (the default) 449 MB, 3.7 s 530 to 690 MB, 9 to 11 s (worst at 60,000 elements)

100,000 is the first power of ten above the densest legitimate shape at the full 1 MB, so no legitimate input measured can reach it within the default request size. It cuts the worst burst's memory by 40 to 55% and its time about fourfold. It also bounds the cost per call when an operator raises requestSizeLimit, which until now raised the element ceiling with it. An operator whose HTTP endpoint is reachable beyond loopback can go lower. At 10,000 the burst costs about 250 MB and 1 s, but a trace shaped like the real agent transcripts would be refused somewhere past 700 KB, and a tools/list-shaped one past about 300 KB.

Why both transports. The advisory says stdio-only servers are not affected. The limit still applies to stdio too, for three reasons. Iris builds stdio and HTTP servers from one factory. Since 0.18.0 one request size limit holds on both, so that a trace is never accepted on one transport and refused on the other. And at the default, no legitimate input measured can reach the limit, so stdio clients see no change.

Measured on Windows 11, i9-12900HK, Node 24.11.0, three runs each (median; the 60,000-object case varied from 530 to 690 MB). To reproduce: node --import tsx scripts/bench-tool-input-elements.ts (options --limits, --concurrency, --runs, --elements). It is a script, not a test, so the default suite stays fast.

Tests

  • tests/integration/tool-input-elements.test.ts, run on the server stdio serves and over real HTTP:

    • a call over the limit gets isError: true, stores nothing, and the session keeps serving
    • a call of exactly the limit passes; one element more is refused
    • at the default, a trace of the densest legitimate shape filling the whole 1 MB request (about 67,000 elements) is stored
    • at the default, 100,001 zeros where spans are expected are refused before validation
    • a config built without the key gets the default

    Five of the six fail with the option removed from src/server.ts. The sixth, the legitimate 1 MB trace, passes either way, as it should.

  • tests/unit/config-tool-input-elements.test.ts: the default; the file and the environment, with the environment winning; values that refuse startup (0, -1, 1.5, 1e5, lots and 10,000 in the environment; 0, -5, 2.5, "100000" and null in the file).

  • tests/unit/middleware/*: their literal security configs gain the new key.

Lockfile

Regenerated on Linux (node:24, npm 11.19.0) with npm install --package-lock-only --ignore-scripts --no-audit --no-fund. Two places changed: the root package's declared SDK range, and the version, resolved and integrity of ip-address. No entry was added or removed (git diff origin/main -- package-lock.json | grep -cE '^-\s+"node_modules/' = 0). The counts of libc fields (10), integrity hashes (392) and @emnapi entries (0) are unchanged. npm audit at the root now reports only GHSA-86w9-cpqp-85rv (node-forge, dev tooling, already recorded), and scripts/security/check-exposure-coverage.mjs passes.

🤖 Generated with Claude Code

…address 10.7.4

Tool calls: the MCP SDK can count the array elements and object keys in a
tool call's arguments before it validates them, but only when the server
sets a limit (GHSA-46jr-4wx8-v453), and Iris set none. Arguments that fail
validation are reported element by element, so one 1 MB request of 524,229
elements cost the server 260 MB, and 20 requests of 1 MB at once up to
1,148 MB and 41 s to answer. Every MCP server Iris builds, on stdio and
HTTP, now sets security.maxToolInputElements (default 100,000; also
IRIS_MAX_TOOL_INPUT_ELEMENTS). A call over it gets an isError result
before validation and the session goes on. The default clears every
legitimate input measured, even one filling the whole 1 MB request (the
densest shape found holds 68,506 elements at 1 MB).

SDK floor: ^1.30.0 -> ^1.32.0. A published package does not carry the
lockfile, so an install that already held 1.30.x or 1.31.x kept it; those
versions are affected by six advisories, and an older SDK ignores the new
option. The lockfile already installed 1.32.0.

ip-address: overrides floor ^10.5.1 -> ^10.7.4 (GHSA-rxp7-6hxx-x8q5);
lockfile 10.7.2 -> 10.7.4, regenerated on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
website Ready Ready Preview Oct 22, 2026 5:35pm UTC

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Iris gate — 1 of 2 tripped --fail-on detector_veto

iris-eval ingest: 3 stored, 1 tripped --fail-on detector_veto (2 of 3 evaluated in dataset "release-gate")

Trace Verdict Basis Rules, classes or missing inputs Evidence
6a93ac4da21f79280e4e1c769fdf5b3f failed detector_veto + risk_over_loss no_pii, pii_leak, credential_leak no_pii: AWS Access Key (output 45–65)
Verdict basis Traces
detector_veto 2
clean 1

Unjudged questions: task_completed (3), tool_use_correct (3) — a trace that did not carry what a rule needs.

tests/fixtures/ci-gate/traces.ndjson · 3 evaluated · dataset release-gate: 2 in the gate · exit 1 · what the bases mean

@github-actions

Copy link
Copy Markdown

Iris gate — 1 stored, nothing tripped --fail-on any

iris-eval ingest: 1 stored, 0 tripped --fail-on any

Verdict basis Traces
clean 1

Unjudged questions: task_completed (1), tool_use_correct (1) — a trace that did not carry what a rule needs.

tests/fixtures/ci-gate/clean.ndjson · 1 evaluated · exit 0 · what the bases mean

@irparent
irparent merged commit 3e0ce06 into main Oct 22, 2026
143 of 144 checks passed
@irparent
irparent deleted the fix/sdk-input-limits branch October 22, 2026 17:52

This branch was successfully deployed

1 active deployment
Preview — 126b0cf7 Deployed Oct 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant