Skip to content

Harden local trust boundary and fix signing-path bugs - #4

Merged
gskril merged 2 commits into
mainfrom
security/harden-boundary-and-signing
Jul 8, 2026
Merged

Harden local trust boundary and fix signing-path bugs#4
gskril merged 2 commits into
mainfrom
security/harden-boundary-and-signing

Conversation

@gskril

@gskril gskril commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Why

The proxy triggers wallet signing and exposes pending-transaction data, but it bound to all network interfaces with wildcard CORS and no Origin/Host validation. That means any website the developer visited — or any machine on the same LAN — could:

  • inject an eth_sendTransaction that auto-opens a wallet-approval tab (a text/plain "simple" request skips the CORS preflight, so the side effect fires regardless),
  • read the to/value/data of in-flight signing requests via the /api/pending enumeration endpoint, and
  • forge a result back to Foundry/Hardhat via unauthenticated /api/complete.

This PR closes that boundary and fixes several correctness bugs in the signing path found during the same review.

What changed

Server — trust boundary

  • Local-only guard (server.ts): rejects requests whose Host isn't loopback (localhost/127.0.0.1/[::1]:<port>, blocking DNS rebinding) or whose Origin is present and foreign (blocking CSRF). Non-browser tools send no Origin and pass through, so Foundry/Hardhat/curl keep working.
  • Bind to 127.0.0.1 (index.ts) so the server is unreachable off-machine; also validates --port.
  • Removed /api/pending enumeration (and dead getAllPendingIds/getPendingCount) — the unguessable UUID is now the only handle to a request.
  • DoS caps: batch size ≤ 50, concurrent pending ≤ 100.
  • /api/complete hardening: guarded JSON parse (clean 400, not 500) and a required boolean success.
  • Timer cleanup on resolve + unref; unknown /api/* returns JSON 404 instead of the SPA page.

Web — signing path

  • personal_sign/eth_sign sign raw bytes for hex messages ({ raw }) instead of the UTF-8 of the "0x…" string, so the signature verifies against what the dapp expects and the wallet popup matches the review screen.
  • Always notify the server on failure (not just on "rejected"/"denied"), so the calling script gets an immediate rejection instead of hanging until the 5-minute timeout.
  • Success-after-send: a failure reporting completion is logged, not shown to the user as a transaction error (prevents duplicate re-submits).
  • Re-entrancy guard (useRef) so a double-click can't broadcast twice.

Verification

  • bun run build (server + web) and tsc --noEmit both clean.
  • Live smoke test against a running server: tool eth_chainId works; cross-origin eth_sendTransaction403; foreign Host403; /api/pending404 JSON (no ID leak); 51-item batch → 400; malformed/empty /api/complete400; valid routes + SPA still 200.

Notes for reviewers

  • The guard treats a missing Origin as allowed on purpose — that's how non-browser dev tools reach the RPC. Browsers always send Origin on cross-origin POSTs, so the CSRF path is covered.
  • Follow-ups from the review left out of this PR: supply-chain hardening (SHA-pin the publish workflow, bun install --frozen-lockfile, the ./types export pointing at unshipped source) and remaining web display-fidelity fixes (chain label vs tx.chainId, formatUnits decimals, generate-chains.ts currency coupling, an error boundary, surfacing nonce/fees).

🤖 Generated with Claude Code

gskril and others added 2 commits July 7, 2026 21:19
The proxy triggers wallet signing and exposes pending-transaction data, but
bound to all interfaces with wildcard CORS and no Origin/Host validation, so
any website the developer visited (or any machine on their LAN) could inject
transactions, read in-flight requests, and forge results back to the dev tool.

Server:
- Add a local-only guard: reject non-loopback Host (DNS-rebinding) and foreign
  Origin (CSRF); non-browser tools send no Origin and still pass through.
- Bind the listener to 127.0.0.1 and validate --port.
- Remove the /api/pending enumeration endpoint that leaked all live request IDs.
- Cap batch size (50) and concurrent pending requests (100) to bound DoS.
- Guard /api/complete JSON parsing and require a boolean success field.
- Clear the request timeout on resolve and unref it; return JSON 404 for
  unknown /api/* paths instead of serving the SPA HTML.

Web:
- personal_sign/eth_sign: sign raw bytes for hex messages instead of the UTF-8
  of the "0x..." string, so the signature matches what the dapp expects.
- Always notify the server on failure so the calling script gets an immediate
  rejection instead of blocking until the 5-minute timeout.
- Treat post-send completion errors as bookkeeping, not transaction failures.
- Add a synchronous re-entrancy guard so a double-click can't double-broadcast.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/browser-rpc@4

commit: 3cdf617

@gskril
gskril merged commit 2d94974 into main Jul 8, 2026
3 checks passed
@gskril
gskril deleted the security/harden-boundary-and-signing branch July 8, 2026 02:33
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