Skip to content

Decide: opening a space link connects to, and authenticates with, whatever relay that link names #79

Description

@usekaneo

From the CON-43 security audit (2026-09-14). Severity medium. This is a decision, not a bug report — the behaviour follows from how NIP-29 addresses work and is shared with every Nostr client that uses relay-hinted addressing. What is missing is that we ever decided it, and that the docs say it.

What happens today

A space address is <relay-host>'<group-id> and lives in the URL: /s/<host>'<group>. The chain is short and fully automatic:

  1. src/ui/layout/AppShell.tsx:37-39 — parseGroupAddress(params.group) turns the route parameter into relayUrl, with no allowlist and no prompt.
  2. useRelay(relayUrl) → client.want(relayUrl) opens the WebSocket.
  3. src/nostr/relay-status.ts:19 additionally fetches https://<host> for the NIP-11 document.
  4. src/nostr/client.ts:172 — this.pool.automaticallyAuth = (url) => this.signAuth(url). If the visitor is signed in, the NIP-42 AUTH event is signed and sent without any interaction.

So following a link is enough to make a signed-in user's browser contact a host of the link author's choosing and hand it a signed statement of who they are. The signature is scoped to that relay per NIP-42 and is not replayable elsewhere, but the pubkey, the IP and the timing are disclosed, and the visitor never chose to trust that relay.

Why this needs a decision rather than a fix

The relay is part of the group's identity in NIP-29 — a space address without a relay host is not an address. Refusing unknown relays outright would mean the app can only ever open spaces on relays already known to it, which breaks the one thing a shareable space link is for. So the plausible answers are not "fix it" but a choice:

  • Accept and document. Treat it like the image decision (docs/09-security-privacy.md → "Images are loaded directly"): a stated, deliberate trade. Cheapest, and defensible — but only once it is actually written down.
  • Ask on first contact. An unknown relay host prompts once ("This space lives on relay.example. Connect?") and is remembered per host. Costs one interaction on a genuinely new space, and removes the drive-by entirely.
  • Connect but do not authenticate. Open the socket to an unknown relay, but withhold AUTH until the user has confirmed the host. Reading a private group then fails until they confirm, which is most of the value — but it does keep the pubkey back, which is the part that identifies a person.

The closed-teams model (reading always needs sign-in and an invitation) argues that visitors will be signed in essentially always, so the disclosure is the normal case, not an edge case.

Scope

  • Pick one of the three and record why in docs/09-security-privacy.md.
  • Implement it, unless the decision is "accept", in which case the docs change is the whole change.
  • Either way, extend the privacy section of docs/09-security-privacy.md: it currently discusses only images, so a reader can reasonably assume relay connections are more guarded than they are. The audit found no place where code and that document contradict each other — this is the one omission.
  • If the decision introduces a known-hosts list, note the interaction with the CSP ticket: a narrow connect-src and arbitrary relay hosts pull in opposite directions, and an allowlist is what would let both exist.

Out of scope

The startsWith prefix bug in the same function is its own ticket and should land regardless of what is decided here.

Acceptance

  • The decision and its reasoning are in docs/09-security-privacy.md.
  • If code changed: the dev setup (localhost:8080) still opens without friction, and tests cover the new path.
  • npm run typecheck && npm run lint && npm test && npm run build pass.

Affected: docs/09-security-privacy.md, possibly src/nostr/client.ts, src/ui/layout/AppShell.tsx, src/nostr/group-address.ts

Source: CON-43, finding A4.


Task: lisl309duhciphb3mt2rq515

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions