Repository navigation
Security: bound validation errors and count request bodies before validating them - #897
Merged
Merged
Conversation
…idating them Validation errors return at most 20 issues and the total. Every answer built from Zod issues goes through one helper (src/utils/validation-errors.ts). A 400 from the dashboard API or the MCP HTTP transport keeps `error` and `details`, lists at most the first 20 issues, and adds `issueCount` and, when issues were left out, `detailsNote`. Within a listed issue, unknown keys are cut to 20 (with `keyCount`), a message to 1,000 characters, a path to 32 segments, and a union's nested errors to a few per branch. The same bound holds for the reasons POST /v1/traces gives for a dropped span (three issues each), IRIS_INVALID_RULE_CONFIG, and the unknown-key and unknown-argument messages of the strict body, query and tool-input schemas. Twenty covers a trace, the largest body the API takes, with all 18 of its top-level fields wrong. Request bodies are counted before any schema reads them. A JSON body on the dashboard port, or a protobuf body on POST /v1/traces as it is decoded, holding more array elements and object keys than the element limit is answered 413 with a short sentence; the protobuf decoder stops at the limit. This is the limit tool calls already have, so the setting is renamed to cover both: security.maxInputElements and IRIS_MAX_INPUT_ELEMENTS (default 100,000). The names it had on main, maxToolInputElements and IRIS_MAX_TOOL_INPUT_ELEMENTS, are still read when the new ones are unset; a config.json that sets both is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
An OTLP/HTTP exporter treats a 413 as final and drops the batch, and decoded protobuf is denser than JSON: the plain Python exporter's bodies (tests/fixtures/otlp/python-plain.pb) hold one element per 7.5 bytes, so a full 1 MB batch holds about 139,600 elements, past the shared limit of 100,000. Such a batch was refused. A protobuf body on POST /v1/traces is now counted, as it decodes, against max(security.maxInputElements, ceil(requestSizeLimit bytes / 5)): 209,716 at the default 1 MB, half again above the densest exporter body measured. Raising requestSizeLimit for a collector's larger batches raises the ceiling with it. OTLP sent as JSON, and every other dashboard body, keep the shared limit. A test sends a 1 MB batch built from the plain exporter's real body and expects it stored whole; it fails with the shared limit in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Iris gate — 1 of 2 tripped
|
| Trace | Verdict | Basis | Rules, classes or missing inputs | Evidence |
|---|---|---|---|---|
7d8a7be38cfdeb9c523ed8e0eca4be62 |
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
Iris gate — 1 stored, nothing tripped
|
| 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This hardens how the dashboard port and the MCP tools answer input that fails validation. It does two things:
1. Validation errors return at most 20 issues and the total
Every answer built from Zod issues now goes through one helper,
src/utils/validation-errors.ts.Dashboard API and MCP HTTP transport (400 responses). The body keeps its fields and gains two:
erroranddetailsstay as they were.detailslists the first 20 issues, in Zod's order.issueCountis new and always present: how many issues there were in all.detailsNoteis new and present only when issues were left out. It is a sentence saying how many are listed of how many.Within one listed issue:
keyCountfield gives the total when there were more.Where it applies:
POST /v1/traces: each dropped span's reason inpartialSuccess.errorMessagenames three issues and how many more. Its 400 gainsissueCount.IRIS_INVALID_RULE_CONFIG: names 20 issues and how many moreWhy 20. The largest body the API takes is a trace, with 18 top-level fields. A trace with every one of them wrong is still answered in full. A request that is wrong in more places than that is almost always wrong in one way many times, and the first twenty show the pattern. A request wrong in a few places gets exactly the answer it got before, plus
issueCount.2. Request bodies are counted before validation
What gets counted:
POST /v1/tracesas JSON), against the shared limitPOST /v1/traces, as it is decoded, against its own ceiling (below). The decoder stops once it has read more nested messages than the ceiling, because every nested message decodes to at least one element.A body over its limit gets a
413before any schema reads it:{ "error", "limit" }. The counting is the MCP SDK's own: array elements plus own object keys, stopping at the limit plus one (src/utils/element-count.ts).One setting, renamed. The shared limit is the one tool-call arguments already have, at the same default of 100,000. Since it now covers more than tool calls, it is renamed:
security.maxInputElementssecurity.maxToolInputElementsIRIS_MAX_INPUT_ELEMENTSIRIS_MAX_TOOL_INPUT_ELEMENTSThe old names are still read whenever the new ones are unset, in
config.json, in the environment, and in a config object built in code. Aconfig.jsonthat sets both names is refused at startup, naming them. The rename is documented in--help, the README,server.json(both variables listed),docs/api-reference.mdand the CHANGELOG.Legitimate bodies, measured. Each is counted the same way, at the full 1 MB request size limit:
POST /api/v1/tracestakes thelog_tracearguments; densest legitimate shape, from #896)tests/fixtures/otlp)Protobuf gets its own ceiling. An OTLP/HTTP exporter treats a
413as final and drops the batch, and the last row is past the shared limit. So a protobuf body is counted againstmax(security.maxInputElements, ceil(requestSizeLimit bytes / 5)):requestSizeLimitfor a collector's larger batches raises it too, and one who wants less on this door lowersrequestSizeLimit.OTLP sent as JSON, and every other dashboard body, keep the shared limit.
What it costs
Measured on a Windows 11 machine (i9-12900HK), Node 24.11.0, with the real dashboard server in its own process.
Refusing a 1 MB body over its limit:
413, 239 B, 0.03–0.06 s413in 12–32 s413, 239 B, 0.04 s413in 11 s413, 239 B, 0.05–0.09 s413in 25 sThe worst JSON body under the shared limit fails validation in all of its nearly 100,000 elements. It is still answered with a reply of about 3 KB.
The protobuf ceiling:
200, 0.27 s, +72 MBTo spend less on that door, lower
requestSizeLimit, or put a collector with its own limits in front.Tests
tests/integration/bounded-validation-errors.test.ts, against the real dashboard server and an MCP client:issueCount: 50000and the note, in under 5 KBerror,detailsandissueCount413and stores nothing, while one of exactly the limit reaches the route413python-plain.pbrepeated with unique ids: 2,538 spans in 254 traces, about 139,600 elements) is stored whole at the defaultsUnit tests:
tests/unit/utils/validation-errors.test.ts: the helper, the error handler, and the rule-definition errortests/unit/config-input-elements.test.ts: the setting, its old names, both names set at once, and the protobuf ceiling (209,716 at 1 MB, 2,097,152 at 10 MB, never below the shared limit)tests/unit/otel/protobuf.test.ts: the decoder stops at the limit and decodes a request that fitsEach part's tests fail without that part. With the pre-count removed, 4 of the body tests fail. With the bounded replies reverted, 7 tests fail. With the shared limit in place of the protobuf ceiling, the 1 MB exporter batch test fails (
413where it expects200), and so does the ceiling test.The whole root suite passes: 400 files, 4,877 tests. No dependency or lockfile changes.
🤖 Generated with Claude Code