Skip to content

fix(go): close all 17 high-divergence parity findings (H1-H17) - #235

Merged
rado0x54 merged 11 commits into
go-backendfrom
go-parity-highs
Jul 5, 2026
Merged

rado0x54 merged 11 commits into
go-backendfrom
go-parity-highs

Conversation

@rado0x54

@rado0x54 rado0x54 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Implements every high-severity item from the Node → Go parity audit of 2026-07-04 (data/go-parity-findings-2026-07-04.md, audited at 8cce700). H17 (key-directory watcher) had already landed in a1fa3ac; the remaining 16 land here as one commit per logical group. Also closes M2 (via a1fa3ac), M4, M12, M17, and the L17 mount-only-with-VAPID half along the way.

Security

  • H2/H3 — file keys: connection-auth file-key signs are now approval-gated (approvedFileKeySigner → key-approve pending action; deny falls through as a skip signature), and file-key selection matches Node exactly — admin-only + ssh_keys.enabled + present on disk — for both session auth and the forwarding agent. File keys are offered before passkeys (M12).
  • H4 — MCP session hijack: session IDs are minted via ServerOptions.GetSessionID and ownership-registered before the client ever sees them; a foreign account replaying an Mcp-Session-Id gets Node's uniform JSON-RPC 404.
  • H14 — passkey clone detection: VerifyAssertion enforces strict counter increase (go-webauthn's Verify never sees the stored counter); covers login, consent, and step-up.
  • H8 — push-endpoint SSRF allowlist (https + FCM/Mozilla/Apple/WNS) wired as rest.Push.AllowedEndpoint.
  • H9 — rate limiting: new internal/ratelimit, enforcing the exact per-route limits Node configures (self-register, invites, hydra login/consent/logout options+verify, step-up, DCR 10/15min) with fastify-shaped 429s and X-RateLimit-*/Retry-After headers.
  • H11 — trustProxy: new internal/realip (Fastify/proxy-addr semantics: bool / hop count / CIDRs / presets); one context value consumed by the /mcp IP allowlist, audit sourceIp, agent-proxy, and the rate limiter — the allowlist no longer fails open behind a reverse proxy.

Lifecycle

  • H1 — account deletion / 90-day cleanup teardown: closes live sessions (account-deleted), drops the account's MCP transports (mcp.Deps.DropAccount, closes M17), revokes Hydra login+consent sessions; both the admin DELETE route and the cleanup job go through the same path.
  • H5 — MCP disconnect: InitializedHandler + ServerSession.Wait() destroy the AgentSession when the MCP session closes, so owned terminals close with agent-disconnect. ClientInfo is captured in the same hook (M4).
  • H6 — idle janitor: 30-min idle timeout, 1-min sweep, reason idle-timeout; SendInput now bumps lastActivityAt (Node parity — input counts as activity).

Wire / infra parity

  • H10 — CORS: reflect-any-origin + preflight 204 before the bearer gate (port of @fastify/cors {origin: true}).
  • H12 — /mcp DNS-rebinding check: go-sdk's localhost protection is disabled — Node has no such check, it breaks standard reverse-proxy deployments, and /mcp is already gated by OAuth scope + the IP allowlist. Recorded as the deliberate decision the audit asked for.
  • H13 — Hydra revoke-all: all=true when unscoped (Hydra v2 400s otherwise) — un-breaks POST /api/auth/sessions/revoke-all and passkey-revoke session invalidation.
  • H16 — audit duration_ms persisted on session close (StatusEvent now carries CreatedAt since terminal transitions remove the session from the registry before subscribers run).
  • H7 — Web Push delivery: new internal/push channel (webpush-go + VAPID) on the approval broker, Node-shaped payloads, 404/410 subscription pruning; push routes now mount only when VAPID is configured (L17).
  • H15 — same-data-dir cutover: store.Migrate adopts a Node/drizzle DB by stamping goose's version table (guarding against half-migrated schemas with an actionable error) and stamps drizzle's journal on Go-created DBs so a Node rollback applies nothing.

Tests

New coverage: MCP hijack + disconnect integration tests, file-key selection/approve/deny unit tests, realip table tests, rate-limit + CORS middleware tests, push payload/prune + validator tests, and migration-adoption tests that replay the real drizzle migrations. go build ./... && go vet ./... && go test ./... clean; pnpm spdx:check clean.

New dependency: github.com/SherClockHolmes/webpush-go v1.4.0 (H7).

Refs #210

Post-review lifecycle hardening (4c5fa9e)

Two deliberate divergences from Node, both fixing inherited flaws:

  • MCP idle-session expiry — new config key mcp.sessionTimeoutMinutes (default 30, 0 disables; Go-only section, Node's non-strict zod schema ignores it). Sessions with no in-flight HTTP activity for that long are closed by the go-sdk timer and unwind through the existing Wait() teardown: AgentSession destroyed, owned SSH sessions closed with agent-disconnect, Mcp-Session-Id invalidated (uniform 404; spec-conforming clients re-initialize). Node keeps transports until DELETE — an unbounded leak and a forever-valid session credential. A live client holding its SSE stream never expires (the timer only runs while no request is in flight).
  • maxOwned cap prunes externally-closed sessions — the AgentSession cap check now drops ids the TerminalManager no longer knows (idle-timeout janitor, server hangup) before enforcing the limit. In Node, N janitor-killed sessions permanently starve the agent's cap until it reconnects.

Also: ownership-registry entries are now created only for sessions that actually bound (registration moved fully into the first-message hook), so a failed server.Connect can't strand an entry.

rado0x54 added 11 commits July 5, 2026 11:10
…idle-timeout; input now bumps lastActivityAt (H6, #210)
…d replay gets uniform 404 (H4), session close destroys the AgentSession so owned terminals close with agent-disconnect (H5); capture clientInfo on initialized (M4) (#210)
…eted), drop MCP transports (DropAccount, M17), revoke Hydra login+consent sessions; 90-day cleanup routes through the same path (H1, #210)
…on, deny falls through as skip) and file keys are admin-only + DB-enabled + on-disk for session auth and forwarding; file keys offered before passkeys (H2/H3, M12) (#210)
…icated surfaces w/ fastify-shaped 429s (H9), CORS reflect+preflight before the bearer gate (H10), server.trustProxy client-IP resolution consumed by allowlist/audit/limits (H11), disable go-sdk localhost DNS-rebinding check on /mcp for reverse-proxy parity (H12) (#210)
…subscription pruning (H7) + push-endpoint SSRF allowlist wired as AllowedEndpoint, routes mounted only with VAPID configured (H8, L17) (#210)
…mping goose's version table (with half-migrated guard), stamp drizzle's journal on Go-created DBs so a Node rollback applies nothing (H15) (#210)
… on both signer paths (closes M11), MCP teardown armed on first message not notifications/initialized, stale Mcp-Session-Id gets uniform JSON-RPC 404, rate-limit the webauthn register routes + bound limiter buckets (5000, throttled prune), single file-key selection per connect, injectable MCP session-id generator, drop dead clientIP indirections (#210)
… 30m, 0=off) — vanished clients unwind fully via the Wait teardown incl. their SSH sessions; maxOwned cap prunes janitor-closed session ids (Node starves); owners entry registered only for bound sessions (#210)
@rado0x54
rado0x54 merged commit 876d3d4 into go-backend Jul 5, 2026
20 checks passed
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