Skip to content

feat(protocol)!: bind the ephemeral key into the ready signature - #222

Merged
chrischall merged 1 commit into
mainfrom
claude/bind-ephemeral-key
Aug 6, 2026
Merged

feat(protocol)!: bind the ephemeral key into the ready signature#222
chrischall merged 1 commit into
mainfrom
claude/bind-ephemeral-key

Conversation

@chrischall

Copy link
Copy Markdown
Owner

Closes the residual recorded in the private advisory GHSA-j6jv-w774-77m6, found while building the mcp-host browser bridge (chrischall/mcp-host#162) and left open by #213 because the fix breaks the wire.

⚠️ BREAKING. PROTOCOL_VERSION 2 → 3. Every package and the browser extension must be updated together; a v2 peer is refused at the hello. Reload the unpacked extension when this ships, or the bridge stops working — loudly, at the handshake, not silently.

The hole

0.4.0's mutual auth proved the extension was the one whose hello arrived. It never committed to the key the session is derived from:

sessionSig = Ed25519Sign(extEdPriv, mcpHelloNonce || extHelloNonce)   // v2
shared     = X25519(mcpIdentityPriv, extensionSessionPub)             // what we derive

So a relay could forward the genuine hellos and the genuine signature — every nonce and identity intact, the pair code unchanged — substitute an extensionSessionPub it held the private half of, and compute the same shared secret as the MCP. Nothing either side checked said otherwise. The peer path made it trivial (it verified nothing at all until #213), but the host path had the same hole from 0.4.0 onward.

The fix

sessionSig = Ed25519Sign(extEdPriv, mcpHelloNonce || extHelloNonce || extensionSessionPub)

One definition — readySignaturePayload() in @fetchproxy/protocol — used by the extension that signs it and by both server paths that verify it, so they cannot drift apart the way the comments about them already had. A relay would now have to sign its own ephemeral key with the extension's Ed25519 private key.

Why no negotiated version

A version-gated variant avoids the break, and hands the attacker the choice: a relay that can rewrite frames can rewrite the field advertising v3 support, and both ends would then agree on the weaker payload. Closing that needs the negotiation itself signed, which is the same wire change with more moving parts. The 0.4.0 precedent applies — hard break, all packages together, old version refused at the hello.

One test changed sides

KNOWN RESIDUAL: a relay can still swap the ephemeral key and share the session was added in #213 with a comment saying it should start failing the day the signature covered the ephemeral key. It did, on the first run. It now performs the same substitution and asserts the refusal, under a name that says so — the residual's own tripwire, doing its job.

Testing

1233 tests, three consecutive clean runs, tsc -b across all six workspaces, npm run build clean. The 128 tests that initially failed were fixtures hand-building v2 hellos and signatures; each now goes through readySignaturePayload, and validate.test.ts asserts v2 is refused rather than downgraded.

After merge: the advisory can be published, and mcp-host's gate 1 (chrischall/mcp-host#162) is satisfiable — its whole premise is a relay that cannot read the traffic, which until now was narrowed rather than true.

Closes the residual recorded in GHSA-j6jv-w774-77m6.

BREAKING CHANGE: PROTOCOL_VERSION 2 → 3. `ReadyFrame.sessionSig` now signs
`(mcpHelloNonce || extHelloNonce || extensionSessionPub)`. Every package and
the browser extension must be updated together; a v2 peer is refused at the
hello.

0.4.0's mutual auth proved the extension was the one whose hello arrived. It
never committed to the key the session is derived FROM. So a relay could
forward the genuine hellos and the genuine signature — every nonce and identity
intact, the pair code unchanged — substitute an ephemeral public key it held
the private half of, and derive the same shared secret as the MCP. Nothing
either side checked said otherwise. The peer path made this trivial (it
verified nothing at all until #208), but the host path had the same hole from
0.4.0 onward.

The fix is one line of definition and three call sites: `readySignaturePayload()`
in `@fetchproxy/protocol`, used by the extension that signs and by both server
paths that verify, so they cannot drift apart the way the comments about them
already had.

No negotiated downgrade, deliberately. A version-gated variant would let the
attacker pick the version — a relay that can rewrite frames can rewrite the
one that advertises support — unless the negotiation itself were signed. The
0.4.0 precedent applies: hard break, all packages together, refuse the old
version at the hello.

One test changed sides rather than being edited: `KNOWN RESIDUAL: a relay can
still swap the ephemeral key and share the session` carried a comment saying it
should start failing the day the signature covered the ephemeral key. It did.
It now asserts the refusal, under a name that says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chrischall chrischall added the security Security-related changes label Aug 6, 2026
@github-actions github-actions Bot added the auto-review Trigger Claude + Copilot review on this PR label Aug 6, 2026
@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @chrischall's task in 4m 1s —— View job


Reviewing PR #222

  • Read CLAUDE.md and repo conventions
  • Read the PR diff
  • Review protocol.ts changes (readySignaturePayload, PROTOCOL_VERSION bump)
  • Review server/host.ts and server/peer.ts changes
  • Review extension-core/background.ts changes
  • Review test coverage changes
  • Post findings

Summary

Verified the core fix end-to-end: readySignaturePayload() in packages/protocol/src/frames.ts is used consistently by the extension (both pairing and auto-trust ready-signing paths) and by both server verification paths (host.ts, peer.ts), with matching raw-byte content on both sides. PROTOCOL_VERSION 3 is enforced as a hard equality check in validate.ts, so v2 is refused with no downgrade path, matching the PR's stated rationale. The flipped "KNOWN RESIDUAL" test in extension-pin-peer.test.ts now correctly asserts refusal instead of recording the old gap, and the host.test.ts bad-ECDH fixture correctly signs over the new payload so it still exercises the intended failure path. Docs (CLAUDE.md, docs/PROTOCOL.md, docs/SECURITY.md) are updated consistently.

One 🟡 nit posted inline: six integration test files (mutual-auth-idb, graphql-query, read-cookies, all-bootstrap-verbs, two-mcps, reconnect) left concatBytes imported but unused after switching to readySignaturePayload(...). Not caught by tsc -b (tests aren't in packages/server/tsconfig.json's include, and noUnusedLocals isn't set) — cosmetic only.

Could not run npm test/tsc -b/npm run build myself in this sandbox (no node_modules, and npm install/git fetch needed approval that wasn't granted) — noted as an open item rather than a finding, relying on the PR's stated clean test/build runs.

Full review posted as a PR comment and one inline comment.

Verdict: warn

generateEd25519,
ed25519Sign,
readySignaturePayload,
concatBytes,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Nit: concatBytes is now an unused import here — the sig-payload construction it used to feed was replaced by readySignaturePayload(...) in this same diff, and nothing else in the file calls it. Same leftover in the other five integration tests touched by this PR: graphql-query.test.ts, read-cookies.test.ts, all-bootstrap-verbs.test.ts, two-mcps.test.ts, and reconnect.test.ts (all import concatBytes but no longer call it). Not caught by tsc -b since packages/server/tsconfig.json only includes src/**/*, and noUnusedLocals isn't set in tsconfig.base.json, but worth a cleanup pass.

Fix this →

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review: bind the ephemeral key into the ready signature

Read CLAUDE.md, walked the full diff (git diff origin/main...HEAD), and traced the fix end-to-end.

Core change checks out. readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) in packages/protocol/src/frames.ts is the single definition used by all three sites that need to agree on it:

  • packages/extension-core/src/background.ts (both the pairing-flow onServerHello path and the auto-trust onApproval path) — signs with the raw ephemeral pub (result.extensionSessionPub / ephemeral.publicKey), matching what's later base64-encoded and sent as ready.extensionSessionPub.
  • packages/server/src/host.ts — verifies with fromB64(frame.extensionSessionPub).
  • packages/server/src/peer.ts — verifies with fromB64(extensionSessionPub) (now threaded through authenticateExtension's new second parameter).

Byte content matches on both ends in every call site I checked, so the signature does what the PR claims: it commits to the key the ECDH actually uses.

PROTOCOL_VERSION bump 2 → 3 is a hard equality check in packages/protocol/src/validate.ts:508 (raw.protocolVersion !== PROTOCOL_VERSION), so a v2 hello is refused rather than negotiated down — matches the "no downgrade path" rationale in the PR body and docs/SECURITY.md.

Test coverage is exactly what a change like this needs. extension-pin-peer.test.ts's KNOWN RESIDUAL test now asserts the refusal it used to record as a gap (rejects.toThrow(/identity refused/), trust.writes empty) — the PR description's claim that "one test changed sides" checks out. host.test.ts's bad-ECDH-payload test correctly signs over the short (8-byte) pub so the signature check passes before the ECDH throw is reached, which is the right way to keep that test exercising the intended failure path under the new payload. All protocolVersion: 2 fixtures across the touched test files were bumped to 3, with the one exception being the intentional negative case in validate.test.ts (asserting v2 is now rejected).

Docs (CLAUDE.md, docs/PROTOCOL.md, docs/SECURITY.md §T-host-MITM) are updated consistently with the code and with each other, and correctly stop describing the residual as open.

One nit (inline on mutual-auth-idb.test.ts:12): six integration test files (mutual-auth-idb, graphql-query, read-cookies, all-bootstrap-verbs, two-mcps, reconnect) still import concatBytes after switching their sig-payload construction to readySignaturePayload(...); grepping each file turns up no remaining call site. Not caught by tsc -b (server's tsconfig.json only includes src/**/*, and noUnusedLocals isn't set), so it's cosmetic leftover from the refactor rather than a build break.

Not independently verified: I don't have node_modules installed in this sandbox and network-touching commands (npm install, git fetch) require approval I wasn't granted, so I could not re-run npm test / tsc -b / npm run build myself. Static review of the diff (including the full server/extension/protocol/test set) found the payload construction and verification consistent everywhere it's used; I'm relying on the PR's stated "1233 tests, three consecutive clean runs" for the dynamic check.

No security or correctness issues found in the actual fix — it closes the gap described (GHSA-j6jv-w774-77m6) the way the PR body describes, and the hard-break handling matches the repo's stated 0.4.0 precedent.

Verdict: warn

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

🟡 Auto-review verdict: warn — The ready-signature fix (binding extensionSessionPub into the signed payload via readySignaturePayload) is correctly implemented and consistently used across the extension and both server verification paths, with matching test and doc updates; one minor unused-import nit found, no important findings.
📋 Tracking follow-ups: #223

@chrischall chrischall added the ready-to-merge Owner has reviewed; arm auto-merge to land when CI is green label Aug 6, 2026
@chrischall
chrischall enabled auto-merge (squash) August 6, 2026 00:17
@chrischall
chrischall merged commit c13aeed into main Aug 6, 2026
18 checks passed
@chrischall
chrischall deleted the claude/bind-ephemeral-key branch August 6, 2026 00:17
chrischall added a commit that referenced this pull request Aug 6, 2026
…rphaned (#224)

The one nit from #223, on a fresh branch — #222 merged while I was
fixing it.

Six integration fixtures (`mutual-auth-idb`, `graphql-query`,
`read-cookies`, `all-bootstrap-verbs`, `two-mcps`, `reconnect`) and
`peer.ts` still imported `concatBytes` after their signature payloads
moved to `readySignaturePayload()`. Checked each rather than trusting
the list: all seven had zero remaining call sites.

`host.ts` keeps its import — it still concatenates for the *peer hello*
signature (`mcpId || sessionNonce`), which is a different payload and
unaffected by #222.

Worth removing beyond tidiness in one case: in `peer.ts` the import was
the last trace of the payload shape that #222 established is a security
bug, sitting a few lines from the code that no longer builds it.

1233 tests, typecheck clean across all six workspaces.

Closes #223

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit that referenced this pull request Aug 6, 2026
🤖 I have created a release *beep* *boop*
---


##
[2.0.0](v1.11.0...v2.0.0)
(2026-08-06)


### ⚠ BREAKING CHANGES

* **protocol:** bind the ephemeral key into the ready signature
([#222](#222))

### Features

* **protocol:** add write_cookies, the one verb that can repair a
rotated session
([#211](#211))
([b2557c2](b2557c2))
* **protocol:** bind the ephemeral key into the ready signature
([#222](#222))
([c13aeed](c13aeed))
* **server:** let a request name the tab that relays it
([#207](#207))
([c5d3f4d](c5d3f4d))
* **server:** pin the extension's identity, and verify it on the peer
path ([#213](#213))
([0eeced7](0eeced7))


### Bug Fixes

* **cli:** let a real filesystem error be itself, not "no extension pin"
([#221](#221))
([c87a864](c87a864)),
closes [#220](#220)
* **cli:** validate --via-tab before connecting, like the request URL
([#210](#210))
([959fcc5](959fcc5))
* **extension:** reattach the write_cookies doc block, and name the
writable cookies as writable
([#215](#215))
([2730c4a](2730c4a))
* **extension:** use the guarded caps local for the cookie heading
([#217](#217))
([f95c832](f95c832))
* **server:** release only our own extension claim, and stop guessing
scoped names
([#219](#219))
([3d90a64](3d90a64)),
closes [#218](#218)
* **server:** type no-tab rejections so they stop reading as version
mismatches ([#205](#205))
([dc30bd9](dc30bd9))


### Refactor

* **server:** drop the concatBytes imports the signature change orphaned
([#224](#224))
([4985ba7](4985ba7)),
closes [#223](#223)

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
chrischall added a commit to chrischall/canvas-parent-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/infinitecampus-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/creditkarma-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/signupgenius-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/ofw-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/opentable-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/zillow-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/redfin-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/resy-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/zola-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
chrischall added a commit to chrischall/honeybook-mcp that referenced this pull request Aug 6, 2026
fetchproxy 2.0.0 closes a MITM in the bridge handshake (advisory
GHSA-j6jv-w774-77m6, fix in chrischall/fetchproxy#222):
`ReadyFrame.sessionSig` now covers the extension's **ephemeral public
key**, not just the two nonces. Under v2 a relay could forward the
genuine hellos and the genuine signature, substitute an ephemeral key it
held the private half of, derive the same shared secret, and read and
rewrite the session — nothing either side checked committed to the key
the ECDH actually used.

That is a wire break (`PROTOCOL_VERSION` 2 → 3), and `^1.7.0` cannot
take a 2.x. Left alone, this MCP would keep speaking v2 and be **refused
at the hello** by a v3 extension — loudly, at the handshake, not
silently. The fleet and the sideloaded extension move together.

**No source change.** The break is inside the handshake;
`FetchproxyServer`'s public API is unchanged. Verified by running this
repo's own suite against 2.0.0.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review Trigger Claude + Copilot review on this PR ready-to-merge Owner has reviewed; arm auto-merge to land when CI is green security Security-related changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant