Skip to content

fix: harden attach, pairing, and relay for v0.7.0 - #54

Merged
frahlg merged 1 commit into
mainfrom
harden/security-review-fixes
Aug 23, 2026
Merged

fix: harden attach, pairing, and relay for v0.7.0#54
frahlg merged 1 commit into
mainfrom
harden/security-review-fixes

Conversation

@frahlg

@frahlg frahlg commented Aug 23, 2026

Copy link
Copy Markdown
Member

Summary

Closes the remaining security-review P0s so attach and pairing work under a hostile relay and LAN.

  • Attach: Noise KK against the pinned host key is part of locator success. LAN dials only private, link-local, or loopback addresses. ICE disconnected does not end a live session. Native attach fetches ephemeral TURN (no Google STUN when TURN is present) and reconnects onto the same tmux session.
  • Pairing: safety number is shown after msg2 so a QR client can compare it before msg3. Live pin-set follows disk without restart. pair-dev pins a base58 owner id, not X25519 hex.
  • Relay: pair bridges are capped in count, duration, and bytes. TURN credentials last 15 minutes with an origin allowlist (never CORS *). A revocation persist failure cannot return 204 and then vanish after restart. SPA deploy refuses a dirty or digest-mismatched web tree.
  • Registry: verified host_pub and signal beat the unsigned local cache.

Test plan

  • cd go && go test ./...
  • cd go && go test -race -count=1 on touched packages
  • cd web && npm test (114)
  • locator KK fallthrough, LAN public-addr refuse, live pin-set, pair-bridge caps, TURN CORS/TTL, revocation persist rollback, SAS-before-msg3 CLI path
  • CI on this PR
  • After merge: tag v0.7.0, deploy mir-signal + SPA to Lightsail with MIR_WEB_SHA256

Note

High Risk
Touches attach authorization, pairing trust, LAN dial policy, TURN/CORS, revocation durability, and relay DoS bounds—security-critical paths under a hostile-relay threat model.

Overview
Closes remaining P0s so attach and pairing hold up against a hostile relay or LAN, and ships Miranda as v0.7.0.

Attach now treats a locator as a win only after Noise KK against the pinned host key (wrong-key or stalled LAN falls through). LAN mDNS addresses must be private/loopback/link-local. ICE disconnected no longer tears down a live session. Native attach prefers ephemeral TURN (no Google STUN when TURN is present) and redials onto the same tmux session. Handshake admit slots are released after KK so authenticated sessions do not starve new attaches. Unpinning an owner cancels its live serve loop.

Pairing splits the handshake so the safety number is shown after msg2 and msg3 is withheld until confirm (CLI + web). pair-dev pins a base58 owner id, not X25519 hex. Verified registry host_pub/signal beat the unsigned local cache.

Relay / deploy: pair bridges are capped by count, TTL, and bytes. TURN creds last 15 minutes with an origin allowlist (never CORS *); coturn denies RFC1918/loopback peers. Revocation persist failure rolls back and cannot 204 a non-durable tombstone. SPA deploy refuses a dirty or digest-mismatched web tarball.

Reviewed by Cursor Bugbot for commit ce34315. Bugbot is set up for automated code reviews on this repo. Configure here.

Locator success now requires Noise KK against the pinned host key, and
LAN dials only private, link-local, or loopback addresses. ICE
disconnected no longer ends a live session. Native attach fetches
ephemeral TURN, omits third-party STUN when TURN is present, and
reconnects onto the same tmux session.

Pairing prints the safety number after msg2 so a QR client can compare
it before msg3 is sent. A live pin-set follows disk without restart.
Pair bridges are capped in count, duration, and bytes. TURN credentials
last 15 minutes with an origin allowlist, never CORS *. A revocation
persist failure cannot return 204 and then vanish after restart.

Verified registry host_pub and signal beat the unsigned local cache.
SPA deploy refuses a dirty or digest-mismatched web tree.
@frahlg
frahlg force-pushed the harden/security-review-fixes branch from bb29e9e to ce34315 Compare August 23, 2026 15:03
@frahlg
frahlg merged commit 63103a5 into main Aug 23, 2026
4 of 5 checks passed
@frahlg
frahlg deleted the harden/security-review-fixes branch August 23, 2026 15:03

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb29e9eda8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/internal/peer/peer.go
Comment on lines +71 to +74
case d.recv <- buf:
case <-d.closed:
default:
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve reliable DataChannel delivery

When more than 64 messages arrive before the consumer drains recv, this default branch silently drops an otherwise reliable SCTP message. These messages contain sequential Noise transport ciphertexts, so dropping one advances the sender's nonce without advancing the receiver's nonce; the next decrypt then fails and terminates the terminal session. This can occur during a burst of terminal output or when the browser/client is temporarily slow, so the callback needs lossless queueing or explicit flow control rather than frame dropping.

Useful? React with 👍 / 👎.

// The cred is fetched per-attach and only grants relay bandwidth (Noise keeps
// content E2E; coturn quotas bound abuse), so a generous TTL is acceptable.
const turnTTL = 12 * time.Hour
const turnTTL = 15 * time.Minute

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep TURN credentials valid for long terminal sessions

For users whose only viable ICE path is TURN, reducing the REST credential lifetime to 15 minutes makes coturn reject later allocation refreshes or permission creation after the embedded username expires. As the comment immediately above notes, this drops long-running terminal sessions; multi-machine attach does not use the new reconnect loop, and even reconnecting clients suffer periodic interruption. The credential lifetime must outlast the supported session duration or be refreshed without interrupting the existing allocation.

Useful? React with 👍 / 👎.

Comment on lines +38 to +42
if err != nil {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(backoff):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return permanent dial errors instead of retrying forever

Every dial error is retried indefinitely here, including deterministic local errors such as an invalid cached host_pub, an unavailable signer/keychain, or malformed signaling configuration. Before this reconnect wrapper, mir attach surfaced those errors immediately; now the command appears to hang until interrupted and never tells the user what must be fixed. Only errors classified as transient/unreachable should enter the backoff loop, while permanent dial errors should be returned.

Useful? React with 👍 / 👎.

defaultMaxAgentSessions = 128
defaultMaxPairRooms = 1024
defaultMaxPairBridges = 64
defaultPairBridgeTTL = 60 * time.Second

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow the pairing bridge to survive human confirmation

The bridge timeout starts as soon as both peers rendezvous, but the new protocol deliberately pauses after msg2 while the user compares the six-part safety number before sending msg3. If that comparison takes more than 60 seconds, the relay closes both sockets and confirmation fails, despite the agent UI advertising a five-minute pairing window. The bridge should use the ceremony's advertised lifetime or apply an idle timeout that accommodates the human confirmation step.

Useful? React with 👍 / 👎.

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ce34315. Configure here.

rt.release()
}
}
defer releaseHS()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Admit burns attach session IDs

High Severity

authorizeOffer records the one-shot session via acceptAttachSession before admit runs. When the handshake semaphore is full, admit fails and the offer is dropped, but the session id stays marked used. A retry of the same offer hits the replay guard and fails, so load that trips the DoS cap can permanently kill otherwise valid attaches until the client gets a brand-new relay session.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce34315. Configure here.

return client.Attach(ctx, m, idn, iceList, *relayOnly)
}, func(ctx context.Context, mc peer.MsgConn, sess *noise.Session) error {
return client.RunInteractive(ctx, mc, sess, m.Name)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reconnect reuses expired TURN

Medium Severity

ResolveICE runs once before ReconnectLoop, while TURN credentials now expire in 15 minutes. Each redial reuses that same iceList. After a long session drop, reconnect can present expired TURN creds and fail on paths that need the relay, even though the agent fetches fresh TURN per offer.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce34315. Configure here.

@cursor
cursor Bot requested review from miravoss26 and wachtelhund August 23, 2026 15:11

@cursor cursor Bot 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.

Left a non-blocking comment; not approved. Cursor Bugbot completed as skipped and reported unresolved findings that need human review. Assigned reviewers wachtelhund and miravoss26.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor cursor Bot 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.

Left a non-blocking comment; not approving. Cursor Bugbot completed with 2 unresolved findings, including a high-severity attach session-id issue, so this needs human review. Reviewers were assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

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