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
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).
- 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.
- Prober guard —
safeRelayUrl check before any WS/HTTP dial, so pre-existing junk in the DB is never probed.
- 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.
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.wss://http://…) and 434 IP-literal hosts.Prober already stopped in prod as mitigation.
Fix
safeRelayUrl(url)(shared, insrc/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).MAX_HARVEST_PER_EVENT, default 100) in the follows + relay-lists hoses — kills the r-tag bomb at the source;logwhen truncating.safeRelayUrlcheck before any WS/HTTP dial, so pre-existing junk in the DB is never probed.scripts/clean-relays.js,npm run clean:relays) — delete existing SSRF/.onion/malformed/IP-literal docs from therelayscollection; report counts.Acceptance
safeRelayUrlrejects all the observed junk classes (unit-tested) and accepts normalwss://host[/path].npm testgreen. 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.