Skip to content

fix: harden c2c relay authorization and error handling - #27

Draft
sidmorizon wants to merge 2 commits into
mainfrom
claude/e2e-transfer-security-review-su3bzg
Draft

fix: harden c2c relay authorization and error handling#27
sidmorizon wants to merge 2 commits into
mainfrom
claude/e2e-transfer-security-review-su3bzg

Conversation

@sidmorizon

Copy link
Copy Markdown
Contributor

Summary

Security review of the Prime Transfer E2EE relay. The transport is genuine end-to-end encryption — the real key is derived client-side from the out-of-band pairing code plus an ECDHE exchange and never reaches the server — so this focuses on the relay's authorization and hardening, not the crypto.

Changes

1. Enforce room membership on the client-to-client relay (main fix)

JsBridgeE2EEServer relayed e2ee-c2c-request / e2ee-c2c-response to the roomId taken from the client envelope without checking the sender had joined that room. Any connected socket that knew a room id could inject c2c traffic into a live session — cancelTransfer (rate-limit exempt) to abort a transfer, verifyPairingCode spam to exhaust the peer's pairing-attempt budget, or a forged response to a pending request. Confidentiality was never at risk (payloads use the pairing-code-derived key the attacker lacks), but it was an unauthenticated disruption primitive.

The relay now drops any c2c message whose sender socket is not a member of the target room (socket.rooms.has(roomId)). Legitimate peers always join via joinRoom before sending c2c, so genuine traffic is unaffected.

2. Stop leaking server stack traces to clients

E2eeError.toJSON() serialized stack onto the payload sent to clients. Removed it; pino still records the stack server-side.

3. Clarify the room encryptionKey

Documented that it is not the E2EE key (clients never use it) and is reserved for a future transport-layer encryption, so it isn't mistaken for the secret protecting user data.

4. Remove misleading unenforced CORS allowlist

The corsOrigins allowlist (read from CORS_ORIGINS) was built but never enforced — the origin callback returned true for every origin. Replaced with an explicit permissive config (same behavior) and documented why Origin is not an auth boundary here: native/desktop clients send no usable Origin, there are no cookie credentials to protect, and Origin is forgeable by non-browser clients.

Testing

Extended test/smoke.ts: a socket that never joined a room cannot inject c2c into it, and the two real members keep communicating afterward.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AuowqYp863fJaNNjEKvhPT


Generated by Claude Code

claude added 2 commits August 27, 2026 09:18
- Reject client-to-client messages whose sender socket has not joined the
  target room, closing an unauthenticated cross-room injection path
  (cancelTransfer / verifyPairingCode / forged-response hijack) that was
  gated only by knowledge of the room id.
- Drop the stack trace from E2eeError.toJSON so server internals are not
  serialized to clients; pino still records it server-side via err.stack.
- Document that the room encryptionKey is not the E2EE key and is currently
  unused, reserved for a future transport-layer encryption.
- Add a smoke-test assertion that a non-member cannot inject c2c traffic.
The corsOrigins allowlist (read from CORS_ORIGINS) was built but never
enforced: the origin callback returned true for every origin, with the reject
branch commented out. Replace it with an explicit permissive config and
document why Origin is not an auth boundary here - the primary clients (native,
desktop file://) send no usable Origin, there are no cookie credentials to
protect, and Origin is forgeable by non-browser clients. Access control remains
the out-of-band pairing code plus the c2c room membership check.
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.

2 participants