Skip to content

Security: relay-harvest SSRF hardening + bomb cap + junk cleanup #19

@melvincarvalho

Description

@melvincarvalho

Incident

The relay directory ballooned to 20,539 entries. Forensics: a handful of crafted kind-10002 events carried thousands of r-tags each (top event: 9,025 tags; six events ≈ the entire directory). My harvesting (phases 2–3) ingested them wholesale, and the phase-4 prober then dialed internal/reserved addresses scraped from those lists.

SSRF exposure (prober ran on the nostr.social host)

  • 192.168.x → 205, 10.x → 30 (e.g. ws://10.0.0.1:9173/), 127.x → 21, localhost → 15, .onion → 1,155.
  • ws://127.0.0.1:* / http://localhost:* would hit the server's own services via the prober's WS connect + NIP-11 HTTP GET. Classic SSRF amplification.
  • Plus 163 double-scheme (wss://http://…) and 434 IP-literal hosts.

Prober already stopped in prod as mitigation.

Fix

  1. safeRelayUrl(url) (shared, in src/hoses/relays.js): canonicalize + reject loopback/localhost, private (10/172.16–31/192.168), link-local (169.254), 0.0.0.0, IPv6 ::1/fc00::/7/fe80::/10, any IP-literal host, .onion, and malformed/empty hosts. Used by both harvest and the prober (defense-in-depth).
  2. Cap harvested URLs per event (MAX_HARVEST_PER_EVENT, default 100) in the follows + relay-lists hoses — kills the r-tag bomb at the source; log when truncating.
  3. Prober guardsafeRelayUrl check before any WS/HTTP dial, so pre-existing junk in the DB is never probed.
  4. One-off cleanup (scripts/clean-relays.js, npm run clean:relays) — delete existing SSRF/.onion/malformed/IP-literal docs from the relays collection; report counts.

Acceptance

  • safeRelayUrl rejects all the observed junk classes (unit-tested) and accepts normal wss://host[/path].
  • A bomb event yields ≤ cap harvested URLs.
  • Cleanup removes the internal/malformed docs; directory drops back toward the ~5.7k real hosts.
  • npm test green. Prober can be safely re-enabled afterward.

Note: URL-level blocking covers literal private IPs (the observed attack). DNS-rebinding (public name → private IP) is a deeper, separate mitigation (resolve-then-check) — out of scope here, noted for follow-up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    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