Repository navigation
fix: stop evaluating server-supplied schema regexes (v1) - #2879
Conversation
A tool's outputSchema and an elicitation's requestedSchema come from
the server, and their `pattern` / `patternProperties` regexes were run
on the native backtracking RegExp engine. A pattern with nested
repetition checked against a near-match value takes exponential time,
freezing the client UI or hanging the CLI (the SDK Client validates
tool output).
Those regexes are now not evaluated at all. A copy of the schema with
every `pattern` removed (nested subschemas included) is compiled; an
object schema's `patternProperties` is dropped together with its
`additionalProperties`, since which keys are "additional" depends on
the regexes. The original schema is not mutated. Every other keyword
still validates, and a note ("N regex pattern(s) in this schema were
not evaluated") is shown with the tool result and on the elicitation
form, and in the CLI on stderr or in the validation error.
The CLI hands the SDK Client a jsonSchemaValidator that strips the
schema and delegates to the SDK's own AjvJsonSchemaValidator, so no
dependency is added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
🟡 Changes recommended
Server-provided regexes remain executable through unsanitized reference targets and the form renderer, leaving the denial-of-service risk unresolved.
5 open issues
Pass the regex-free schema to DynamicJsonForm · New Sanitize or reject references to regex-bearing schema locations · New Preserve additionalProperties when patternProperties is empty · New Preserve or rewrite references before deleting patternProperties · New Avoid changing validation semantics in regex-dependent compositions · New
What changed in this PR
This PR implements the v1 mitigation for GHSA-2fw8-7jmw-r937 by stripping server-supplied schema regexes before validation and reporting skipped checks. Unlike the v2 fix, it avoids adding a bounded-time regex engine.
Changes:
- Adds schema-copying helpers and integrates them into web and CLI validation.
- Displays skipped-pattern notices in tool results, elicitation forms, and CLI diagnostics.
- Adds regression tests for malicious patterns, remaining constraints, schema preservation, and SDK integration.
| File | Description |
|---|---|
client/src/utils/stripSchemaPatterns.ts |
Adds schema sanitization and notice formatting. |
client/src/utils/schemaUtils.ts |
Sanitizes tool schemas and tracks skipped patterns. |
client/src/utils/__tests__/stripSchemaPatterns.test.ts |
Tests traversal, keyword removal, and immutability. |
client/src/utils/__tests__/schemaUtils.test.ts |
Tests skipped patterns and remaining validation. |
client/src/components/ToolResults.tsx |
Displays skipped-pattern notices with results. |
client/src/components/ElicitationRequest.tsx |
Sanitizes submit validation and adds a notice. |
client/src/components/__tests__/ToolsTab.test.tsx |
Tests the tool-result notice. |
client/src/components/__tests__/ElicitationRequest.test.tsx |
Tests elicitation notices and validation behavior. |
cli/src/utils/stripSchemaPatterns.ts |
Adds the CLI schema sanitizer. |
cli/src/utils/regexFreeSchemaValidator.ts |
Wraps SDK validation with sanitization and notices. |
cli/src/index.ts |
Registers the validator and sends notices to stderr. |
cli/__tests__/regexFreeSchemaValidator.test.ts |
Tests the wrapper and end-to-end tool calls. |
Review effort: Balanced
…ilure
Address review findings on the regex-stripping helper (client and cli
copies kept identical):
- Re-point every $ref at a sanitized schema position, or remove it: refs to
non-schema locations (e.g. #/default), remote/plain-name refs, refs in a
document with $id, and $dynamicRef/$recursiveRef are removed and counted.
- Keep removed subschemas (patternProperties entries, dropped compositions)
resolvable by moving them, sanitized, into root definitions and rewriting
references to them, so the transformed schema always compiles.
- Preserve additionalProperties beside an empty patternProperties.
- Track loosened ("tainted") subschemas through children and refs, and drop
not / oneOf / if-then-else / contains / unevaluated* whose verdict a
removed constraint could flip, keeping independent siblings.
- Pass the regex-free schema to DynamicJsonForm so no server pattern reaches
native input constraint validation.
- The note now also reports how many dependent checks were skipped.
Refs #2878
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 1 (review 5775588134): five findings, all fixed in 89300ee and answered in each thread. The client and cli copies of
Checks: |
There was a problem hiding this comment.
🟡 Changes recommended
Repeated per-child container cloning makes schema sanitization quadratic, blocking the UI thread and CLI event loop for large schemas.
1 open issue
5 resolved since last review
Sanitize or reject references to regex-bearing schema locations Pass the regex-free schema to DynamicJsonForm Avoid changing validation semantics in regex-dependent compositions Preserve or rewrite references before deleting patternProperties Preserve additionalProperties when patternProperties is empty
Review effort: Balanced
Each emitted child re-copied its whole properties map or subschema array, and the taint pass re-scanned every position until a fixed point, so a wide or deep schema cost quadratic time synchronously. Now each container is copied once per node, positions are numbered instead of keyed by path, taint spreads along reversed parent/referrer edges with a work list, and rewritten $ref pointers are built once per referenced position. The elicitation form memoizes the sanitized schema per request instead of re-sanitizing on every render. Refs #2878 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 2 (review 5775855544): one finding, fixed in 248ba5f (reply). The client and cli copies are still byte-identical.
Checks: |
There was a problem hiding this comment.
🟡 Changes recommended
The schema transform can cause relocated relative IDs to collide and activate constraints previously ignored alongside $ref, breaking validation and potentially blocking web submissions or CLI operations.
2 open issues
1 resolved since last review
Review effort: Balanced
…node
Two cases where the rewrite could make a schema stricter:
- Relocating subschemas could move $id scopes so ids collide. A schema
declaring $id, id, $anchor, $dynamicAnchor or $recursiveAnchor in any
schema position is now not validated at all; callers get validate: false
and a "not validated" note (web: no validator; cli: results pass with the
note on stderr).
- Removing only an unresolvable $ref activated siblings Ajv 6 ignores. The
whole node now becomes {}, its subschemas kept resolvable, counted as
skipped.
States the invariant (output accepts everything the input accepts, or
validation is skipped and reported) and adds corpus tests that check it
under Ajv 6 (client) and Ajv 8 (cli).
Refs #2878
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 3 (review 5776046271): two findings, fixed in fe7ffd9 and answered in each thread. Instead of more rewriting, the helper now holds one invariant, stated in its header: the output schema accepts everything the input accepts, or validation is skipped and reported.
Checks: |
hasOutputSchema() checked only the validator map, so a tool whose schema declares identifiers (cached note, no validator) skipped ToolResults' validation block: the "not validated" note never showed and missing structured content went unreported. It now also checks the note map, and the result reads "Not validated against output schema" rather than "Valid". The note is cached only after a successful compile, so a schema that fails to compile stays absent as before. Refs #2878 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 4 (review 5776208596): one finding, fixed in 2bc33dc (reply). Checks: |
Ajv 6 splits a JSON pointer on "/" before percent-decoding, so `#/definitions/x%2Fy` names the key "x/y"; Ajv 8 decodes first and reads "x" then "y". With both locations present the two resolve different schemas, and rewriting to one of them could reject a value the other engine accepted. Such a ref (and one carrying a NUL, which could collide with internal child keys) now drops its node like any unresolvable ref. Refs #2878 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: cliffhall <cliff@futurescale.com>
|
Copilot round 5 (review 5776329825): no inline comments. Its headline ("a reference-resolution ambiguity … can silently select the wrong schema") was real, and it is fixed in 002c413.
Checks: |
There was a problem hiding this comment.
🔵 Needs a closer look
Security-sensitive reference and composition rewrites across browser Ajv 6 and SDK validators require maintainer review and independent runtime validation to confirm regex exclusion and preserved schema acceptance.
No open issues 🎉
Review effort: Balanced


Closes #2878
The v1 fix for GHSA-2fw8-7jmw-r937, developed and reviewed in the advisory's private fork (inspector-GHSA-2fw8-7jmw-r937#1) and landed here as an ordinary PR. The commit is the fork branch's, unchanged. The v2 fix is #2877.
This is the v1-line fix. Server-supplied
patternandpatternPropertiesregexes are no longer evaluated, and the UI and CLI say so visibly.Problem
A tool's
outputSchemaand an elicitation'srequestedSchemaboth come from the server. Ajv compiled their regexes with the native backtrackingRegExp, so a pattern like^(a+)+$, checked against a near-match string, takes exponential time. Three v1 paths reach it:schemaUtils.validateToolOutput)ElicitationRequest)client.callToolmakes the SDK validatestructuredContentitselfApproach: don't evaluate them
utils/stripSchemaPatterns.tsreturns a copy of the schema with everypatternremoved. It walks the nested subschemas:properties,items(both forms),additionalProperties,allOf/anyOf/oneOf/not,$defs/definitions,dependencies,propertyNames,if/then/else, and so on. It never mutates the original, because the UI displays that object.patternPropertiesis dropped together with that object'sadditionalProperties. Which keys count as "additional" depends on the regexes. Dropping onlypatternPropertieswould makeadditionalProperties: falsefalsely reject keys the server declared, so neither keyword is evaluated. Declaredpropertiesstill are.RegexFreeJsonSchemaValidatoris passed asjsonSchemaValidatortonew Client(...). It strips the schema, then delegates to the SDK's ownAjvJsonSchemaValidator(@modelcontextprotocol/sdk/validation/ajv).Trade-off: on v1, a value that violates a server's
patternis no longer reported as a mismatch. The note makes the gap visible instead of implying the check passed. v1 is security-fix-only, so a small removal beats shipping a regex engine.Tests
stripSchemaPatterns.test.ts:patternsurvivespatternPropertiesandadditionalPropertiesare dropped togetherschemaUtils.test.ts:^(a+)+$returnsisValid: truewith the noteToolsTab.test.tsx: the note renders with the result.ElicitationRequest.test.tsx: the note renders, a near-match submits, and minLength is still enforced.cli/__tests__/regexFreeSchemaValidator.test.ts:Client.callTooloverInMemoryTransportthat returns the near-match result and emits the noteResults
npm run lint: passednpm test: 36 suites, 615 tests, all passingnpm run build: passednpm run test-cli: 5 files, 90 tests, all passing🤖 Generated with Claude Code