Skip to content

Security advisory: phonebook-scrape pairing vulnerability in v0.5.9-v0.5.13 (fixed in v0.5.14) #8

Description

@laulpogan

Summary

A vulnerability in the zero-paste wire add handle flow allowed a malicious actor with access to a public phonebook to deposit authenticated write capability against every wire user listed there. Fixed in v0.5.14 by restoring bilateral-required wire add semantics.

Affected versions

v0.5.9 through v0.5.13 (all v0.5.x releases that shipped the zero-paste handle flow). v0.4.x and earlier (SPAKE2-only pairing) are unaffected.

Severity: Medium

  • Unauthenticated remote write to any phonebook-listed user's slot, up to slot quota (64 MB total, 256 KiB per event).
  • Not remote code execution.
  • Not impersonation (Ed25519 sigs over canonical JSON still verify — the attacker is a known sender, not a forger).
  • Not inbox read access (slot read requires the slot_token, which is never leaked by the relay; the vuln leaks it via the daemon's ack).

Technical detail

Attack chain on v0.5.13

  1. GET https://wireup.net/v1/handles returns every claimed handle + their slot_id (public, by design — Mastodon-style discovery).
  2. For each handle, attacker POSTs a self-signed pair_drop (kind=1100) to the auth-free /v1/handle/intro/<nick> endpoint (src/relay_server.rs:1569). Relay validates: kind, embedded card signature, size cap. No bearer auth required by design.
  3. Relay appends the event to the target's slot. Target's daemon pulls.
  4. maybe_consume_pair_drop (src/pair_invite.rs:489) auto-pinned the stranger's card with tier VERIFIED in trust.json, saved their slot_token in relay_state.json, then POSTed pair_drop_ack carrying the receiver's slot_token back to the attacker.
  5. Attacker now has bearer credential for /v1/events/<receiver-slot>. They can POST arbitrary signed events (any allowed kind, up to slot quota) to the receiver's inbox.

Root cause

The original v0.5 design intent was bilateral: wire add fires on both sides, capability flows after both sides gesture consent. The implementation collapsed the receiver-side gesture into automatic acceptance, making wire add a unilateral grant of write access.

Fix in v0.5.14

maybe_consume_pair_drop now bifurcates on nonce presence:

  • SPAKE2 invite-URL path (pair_drop carries pre-shared pair_nonce): unchanged. The nonce IS the consent gesture — the operator gave it to the sender out-of-band.
  • Handle path (no nonce, zero-paste wire add): write to new state/wire/pending-inbound-pairs/<handle>.json store. No trust pin. No ack. No slot_token leak. OS toast prompts the operator to run wire add <peer>@<their-relay> to accept or wire pair-reject <peer> to refuse.

cmd_add now checks pending-inbound first; if present, runs cmd_add_accept_pending which pins peer VERIFIED with stored coords, sends pair_drop_ack carrying our slot_token, deletes the pending record. Otherwise → outbound path (unchanged).

After v0.5.14, scraping the phonebook + spraying produces N entries in N victims' pending-inbound queues with zero VERIFIED pins, zero slot_token leaks, zero write capability. Each victim sees one OS toast per attacker; victims who don't manually wire add back are fully protected.

Mitigations for users on v0.5.13 or earlier

Upgrade:

cargo install slancha-wire
wire upgrade    # respawns daemon at v0.5.14

If you cannot upgrade immediately: set accept_unknown_pair_drops: false in \$WIRE_HOME/config/wire/policy.json to disable open-mode discovery. This loses the zero-paste UX (only SPAKE2 invite-URL pairing will work) but closes the auto-pin vector.

To audit existing trust state: wire peers --json lists every VERIFIED peer. Compare against your expected pair partners. Any peer you don't recognize is a candidate for revocation (manual edit of \$WIRE_HOME/config/wire/trust.json).

Threat-model addendum + remaining residual windows

docs/THREAT_MODEL.md updated with the bilateral-pair doctrine. Residual windows worth tracking:

  • Retrospective: v0.2.4 SPAKE2 pair finalized asymmetrically (joining side paired, host got nothing) — cross-checked vs v0.5.12 #5 — Third-leg ack race in SPAKE2 finalize. If host's pair_get loop times out at `T+timeout-ε` but the relay already buffered joining's bootstrap, joining can still receive host's previously-buffered bootstrap and flip to paired. Small window, not closed by this release.
  • Relay-side rate limit on /v1/handle/intro/<nick> is per-source via tower_governor but not per-recipient. A high-volume sprayer at peak times could still trigger many OS toasts before the global limit kicks in. Tracking for v0.5.15.
  • wire block <peer> primitive doesn't exist yet. Today, blocking a key means manual trust.json edit. Tracking for v0.5.15.

Credit

Vulnerability surfaced during pre-release security review on 2026-05-17 by the operator + a four-persona adversarial threat-model session (paranoid-adversary / principled-protocol-designer / pragmatic-operator-UX / spam-abuse-researcher). All four converged on the same root cause: capability emitted to an unauthenticated requester on first contact.

No known exploitation in the wild — wire is at <50 users, the phonebook fits on one screen, and abuse would have been visible in inbox traffic. The vuln existed in shipped code for 5 days (v0.5.9 onward); patch released same day as discovery.

Closing this issue when v0.5.14 has been validated on multiple operators' machines + adopted by majority of paired peers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions