Skip to content

chore(release): merge v2/main into main for the v2.14.0 security fixes - #2882

Merged
cliffhall merged 21 commits into
mainfrom
v2/chore/2881-milestone-merge-v2.14.0-security
Nov 4, 2026
Merged

cliffhall merged 21 commits into
mainfrom
v2/chore/2881-milestone-merge-v2.14.0-security

Conversation

@cliffhall

@cliffhall cliffhall commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Closes #2881

Second milestone merge of v2/main into main for v2.14.0. #2867 merged the milestone's work, and this one brings in the three security fixes that landed on v2/main afterwards:

The release is held so that 2.14.0 ships with them.

This is a pure merge

$ git rev-list HEAD --not origin/main origin/v2/main
cca6d4a6 chore: merge v2/main into main for the v2.14.0 security fixes (#2881)

$ git rev-parse origin/v2/main^{tree}
9dd4faed9a645fbadd9d5e31a38b529262ea1f65
$ git rev-parse HEAD^{tree}
9dd4faed9a645fbadd9d5e31a38b529262ea1f65

Verification

Ledger: https://claude.ai/artifact/4TfnRPjxJDedDzQNArH6ma. It now has a "Security fixes" section for this merge, and the milestone stands at 23 / 23.

🤖 Generated with Claude Code

cliffhall and others added 21 commits October 29, 2026 00:10
The registry server.json import panel labelled each option by its package
identifier only, so the command and arguments that would actually be saved
and spawned (runtime arguments included) were never shown before Add Server.

- The panel now shows the exact shell-quoted command line (or remote URL) and
  the env var names of the selected option, including when there is only one
  package. It is built from the same buildServerConfigForSelection output that
  submit persists.
- A package whose identifier or version starts with "-" or contains whitespace
  is skipped like an unsupported package, since either would reach the
  runtime as a flag rather than as the package reference.

Runtime arguments themselves are unchanged: registry runtimeArguments are
legitimate (e.g. docker -v), so they are surfaced rather than filtered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
The URL elicitation modal passed the server-supplied URL to window.open
without checking its scheme. It now uses the shared isHttpUrl allowlist: a
non-http(s) URL disables Open in Browser and the panel says why, and the open
handler re-checks before calling window.open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
wrapHtmlWithCsp located the first <head> with a regex, which can match a
<head> inside a comment, script or attribute and splice the policy where the
parser never applies it. The meta is now written as the first token (after a
leading doctype, if any), so the parser always places it at the top of the
head, ahead of any of the document's own markup. Full documents keep their
own head and <html> attributes; the input is not wrapped in a fixed body.

A Storybook play function asserts the parsed placement in a real browser,
since happy-dom does not place a leading meta the way the HTML spec does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
A frame sandboxed with allow-scripts may always navigate itself, and the
sandbox flags follow it, so the page it lands on keeps the opaque "null"
origin and the same contentWindow. That page passed every identity check
the sandbox proxy's relay makes, received host messages relayed with "*",
could post requests back to the host, and ran under none of the app's CSP.
The proxy's own comment claimed the opposite.

The proxy now installs a frame-src / child-src policy on its own document
before loading the inner frame, built per render from the approved
frameDomains (or the dedicated app origin), so a navigation to an
undeclared origin is blocked before any request leaves the browser. The
host passes the approved grant in the resource-ready message's spec `csp`
slot for this. As defense in depth, a second load on the srcdoc frame
closes the relay in both directions, removes the frame and reports an
error to the host through the app logging channel.

Adds smoke:web:sandbox, an engine smoke that drives the real proxy from
the prod server with a stand-in host and asserts both controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
A tool's outputSchema (and an app elicitation's requestedSchema) comes
from the server, and its `pattern` / `patternProperties` regexes were
run by Ajv on the native backtracking RegExp engine. A pattern with
nested or ambiguous repetition evaluated against a near-match value
takes exponential time on the thread doing the validation.

InspectorClient now validates those schemas with
LinearPatternJsonSchemaValidator: the SDK's Ajv configuration (same
per-dialect engines, options, formats and error text) with Ajv's
`code.regExp` hook routed to a linear-time matcher (core/json/
linearRegExp.ts, a Pike VM whose single-character atoms are decided by
the native engine against one code point). Matching costs input length
x program size, and the total per validation is capped. A pattern that
cannot be evaluated (lookaround, backreferences) or would exceed the
cap is reported as unverified, never as valid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Firefox blocks a frame-src-refused navigation without committing an
error page, so no second load ever fires there and the host was never
told. The proxy now also listens for the policy violation its own frame
policy raises, which both Chromium and Firefox report, and closes the
frame on whichever signal arrives first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
A declared environment variable name is passed to docker run as `-e NAME`
for OCI packages, so a name such as `--privileged` would be read as a docker
flag. parseEnvVars now requires each name to match ^[A-Za-z_][A-Za-z0-9_]*$
(the registry schema types it as a bare string, so this is the POSIX shape).

Behavior chosen: an invalid name rejects the whole package, the same way a
flag-shaped identifier or version does, rather than dropping that one
variable. Dropping it could silently remove a required variable and launch a
misconfigured server; rejecting it leaves the document with the existing
"No runnable package or remote" error when nothing else is runnable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Any doctype other than a plain <!DOCTYPE html> preceded by HTML whitespace
now gets the meta written at the very start of the document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Add is disabled, and submit refuses, while the live text differs from the
debounced text the launch preview was built from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
A counted repeat of a body that emits no instructions, such as
`(){1000000000000}` or `(a{0}){1000000000000}`, looped `min` times without
ever reaching the program-size cap, so compilation could stall before the
evaluation budget applied. Stop expanding once a copy emits nothing (further
copies are equivalent), and cap total AST node visits at MAX_COMPILE_STEPS so
no repeat can cost unbounded work regardless of what its body emits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
The per-pattern compile limits restarted for every pattern, so a schema of
many distinct near-limit patterns could build millions of instructions
before any value was validated. Charge every pattern a schema compiles to
one SCHEMA_COMPILE_BUDGET (node visits and program size); once it runs out
the remaining patterns are declined, and every validation against that
schema reports it could not be verified.

Also accept quantified groups around an assertion such as `(\b)?a`,
`(^)*a` and `($){0}a`, which the native engine accepts: a bare quantified
assertion is already a native syntax error, so the parser's guard only ever
rejected valid patterns. They are added to the differential corpus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
The proxy's load watch assumed the srcdoc fires the frame's first `load`.
A navigation started while the srcdoc is still loading aborts it, so the
destination's `load` is the first and was ignored, leaving the relay open
to the replacement document. The host wrapper now runs a script ahead of
the app's code that posts a departure message on `beforeunload` and
`pagehide`; the proxy closes the relay and removes the frame on it.

The sandbox smoke gains a case that navigates while a never-finishing
nested frame holds the srcdoc's load, and the declared-domain cases now
require a nested frame to the declared domain to be fetched, so a policy
refusing every navigation can no longer pass them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
…-json-import-hardening

fix: harden server.json import review, URL elicitation scheme, and preview CSP injection
…dget

Ajv compiles a schema registered by $id, including a $defs entry of an
earlier schema, on its first getSchema lookup. That lookup ran with no
compile budget active, so it bypassed the schema-wide limit and the
over-budget tracking. Run the lookup and the compile under one tracked
budget, and make a budget mandatory at the regex hook: outside a tracked
entry point a provider-lifetime budget applies, so no path compiles a
pattern unbudgeted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
CodeQL flagged JSON.stringify output spliced into script source in the
departure script and the sandbox smoke (js/bad-code-sanitization), and a
case-sensitive script-tag regex in a test (js/bad-tag-filter). The values
are host constants, but escape <, > and the line separators anyway so no
value can close the element or end a statement, and match the tag
case-insensitively.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
CodeQL's js/bad-tag-filter flags any script-tag regex. The wrapper writes
these bytes itself, so slice between the known tags instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
Decline a pattern longer than MAX_PATTERN_LENGTH (8192) before the native
syntax check or the parser sees it, and charge every pattern's length to
the shared compile budget up front, so many long but cheap patterns add up
against the schema-wide limit and an exhausted budget skips the native
constructor too. The parser builds a constant number of AST nodes per
character, so the length charge also covers parsing.

Read quantifier braces with a sticky regex at the parser's position
instead of slicing the rest of the pattern at every `{`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
…x-frame-navigation

fix(web): stop an MCP App navigating its sandbox frame elsewhere
…-schema-regex-dos

fix(core): evaluate server-supplied schema patterns in bounded time (v2)
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Nov 4, 2026
Copilot AI balanced review requested due to automatic review settings November 4, 2026 22:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The security-sensitive custom regex engine and browser sandbox controls require maintainer validation of pending merge-tree, tarball, and production-browser smoke checks.

No open issues 🎉

What changed in this PR

This second v2.14.0 milestone merge brings three security fixes from v2/main into the release branch, covering the web client’s sandbox and input handling plus schema validation shared by all clients.

Changes:

  • Restrict MCP App frame navigation, close relays after departure, and add cross-browser regression smokes.
  • Harden server.json import review, elicitation URL schemes, and HTML preview CSP placement.
  • Replace native schema-pattern matching with bounded compilation and evaluation, reporting unchecked patterns as unverified.
File Description
scripts/​smoke-web-sandbox-navigation.mjs Adds production-proxy navigation regression smokes.
scripts/​run-engine-smokes.test.mjs Checks sandbox smoke registration and command mapping.
scripts/​run-engine-smokes.mjs Includes sandbox navigation in engine smoke runs.
scripts/​lib/​headless-browser.mjs Updates comments to include the new smoke.
package.json Adds the sandbox smoke command.
docs/​quality-gate.md Documents expanded browser smokes and port usage.
core/​mcp/​toolOutputValidation.ts Documents the bounded-time validator provider.
core/​mcp/​linearPatternValidator.ts Adds budgeted schema-pattern validation and failure reporting.
core/​mcp/​inspectorClient.ts Uses the bounded validator for tool output and app elicitation.
core/​mcp/​import/​serverJson.ts Rejects unsafe package tokens and invalid environment names.
core/​json/​linearRegExp.ts Adds bounded regular-expression compilation and matching.
clients/​web/​static/​sandbox_proxy.html Enforces frame policy and closes relays after navigation.
clients/​web/​src/​utils/​sandbox-csp.ts Adds an early departure-notification script to app wrappers.
clients/​web/​src/​utils/​sandbox-csp.test.ts Tests departure notifications and safe script serialization.
clients/​web/​src/​utils/​commandLine.ts Adds shell-quoted launch formatting.
clients/​web/​src/​utils/​commandLine.test.ts Tests argument quoting and command formatting.
clients/​web/​src/​test/​integration/​mcp/​import/​serverJson.test.ts Covers import rejection rules and preserved runtime flags.
clients/​web/​src/​test/​core/​mcp/​linearPatternValidator.test.ts Tests validator parity, budgets, and unverified results.
clients/​web/​src/​test/​core/​mcp/​inspectorClient-raw-wire.test.ts Covers catastrophic-pattern handling through tool calls.
clients/​web/​src/​test/​core/​json/​linearRegExp.test.ts Tests regex parity, unsupported syntax, and work limits.
clients/​web/​src/​hooks/​useServerJsonImport.ts Builds launch previews and blocks submission during pending validation.
clients/​web/​src/​hooks/​useServerJsonImport.test.tsx Tests launch previews and stale-preview submission refusal.
clients/​web/​src/​components/​groups/​ServerImportJsonModal/​ServerImportJsonModal.tsx Passes the launch preview into the import panel.
clients/​web/​src/​components/​groups/​ServerImportJsonModal/​ServerImportJsonModal.test.tsx Verifies edits disable Add until validation completes.
clients/​web/​src/​components/​groups/​PendingClientRequestModal/​PendingClientRequestModal.tsx Restricts elicitation URL opening to HTTP(S).
clients/​web/​src/​components/​groups/​PendingClientRequestModal/​PendingClientRequestModal.test.tsx Covers refused URL schemes and malformed URLs.
clients/​web/​src/​components/​groups/​ImportServerJsonPanel/​ImportServerJsonPanel.tsx Displays resolved commands, URLs, and environment names.
clients/​web/​src/​components/​groups/​ImportServerJsonPanel/​ImportServerJsonPanel.test.tsx Tests launch-preview display and omission.
clients/​web/​src/​components/​groups/​ImportServerJsonPanel/​ImportServerJsonPanel.stories.tsx Adds launch previews to import examples.
clients/​web/​src/​components/​groups/​ElicitationUrlPanel/​ElicitationUrlPanel.tsx Displays URL refusals and disables opening.
clients/​web/​src/​components/​groups/​ElicitationUrlPanel/​ElicitationUrlPanel.test.tsx Tests refusal messaging and retained copy access.
clients/​web/​src/​components/​elements/​ContentViewer/​contentViewerUtils.ts Places preview CSP before untrusted markup.
clients/​web/​src/​components/​elements/​ContentViewer/​contentViewerUtils.test.ts Covers deceptive head tokens and doctype edge cases.
clients/​web/​src/​components/​elements/​ContentViewer/​ContentViewer.stories.tsx Checks CSP placement with a real browser parser.
clients/​web/​src/​components/​elements/​AppRenderer/​createAppBridgeFactory.ts Sends approved CSP grants to the sandbox proxy.
clients/​web/​src/​components/​elements/​AppRenderer/​createAppBridgeFactory.test.ts Verifies only approved frame domains reach the proxy.

Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review loop closed. Round 1 (review 5776887795) was clean, with no inline comments and no open issues. Its headline asks for the merge-tree, tarball and production-browser checks. The tree check is in the body (9dd4faed on both sides). local:gate, pack:verify and the three fixes' smoke rows are running, and the results will be added here and to the ledger.

@cliffhall
cliffhall merged commit 4d3464c into main Nov 4, 2026
9 checks passed
@cliffhall
cliffhall deleted the v2/chore/2881-milestone-merge-v2.14.0-security branch November 4, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release 2.14.0: merge v2/main into main again for the security fixes

2 participants