Skip to content

gossip store has no resource bounds: unbounded relay-hint accumulation (memory + SQLite) via connected relay #1433

Description

@Datawav

Describe the bug

The gossip store accumulates relay hints and public-key entries without any
effective bound, so a malicious or compromised relay can drive unbounded memory
(in-memory store) or disk (SQLite store) growth in any client that enables gossip.
This is a single root cause — missing resource bounds in the gossip store — that
shows up in three places. All line numbers at pinned commit
9ad7b273e7f4c460d07feea0077b3a4491dcbc6a.

1. Per-key relay hints are uncapped (in-memory).
gossip/nostr-gossip-memory/src/store.rs:131 — the catch-all _ => arm scans every
valid event's p tags and inserts each attacker-supplied relay hint into the
referenced public key's PkData.relays: IndexMap (struct at line 34-37) via
update_relay_per_user (lines 377-394). There is no per-key cap or eviction. The
NIP-65 and NIP-17 arms are capped (.take(MAX_NIP65_SIZE) / .take(MAX_NIP17_SIZE),
= 7 at constant.rs:9-10), but the HINT arm is not. NostrGossipMemory::bounded
(line 65) only bounds the number of public keys via the outer LruCache, not the
number of relays within one key — and repeated events referencing a victim key keep
it LRU-hot, so the outer cache does not mitigate per-key growth.

2. SQLite gossip store has no quota or GC.
gossip/nostr-gossip-sqlite/src/store.rs:573update_hints (called from
process_event, line 112) inserts into public_keys, relays, and
relays_per_user with no row/byte/age/per-key quota and no garbage collection.
Repeated events with unique referenced keys and unique hint URLs grow the database
without bound (the WAL may also become large between checkpoints).

3. unbounded() constructor offers no safety limit.
gossip/nostr-gossip-memory/src/store.rs:57NostrGossipMemory::unbounded()
builds LruCache::unbounded(). It is an explicit opt-in (there is no Default;
bounded(limit) exists), so this is lower severity, but neither the docs nor the
examples mark bounded() as the recommended production path.

Reachability. nostr-sdk/src/relay/inner.rs:1322-1325 calls
gossip.process(&event, ...) for every verified event received from any connected
relay. Relay-level limits cap per-event tags (~2000) and message size (~5MB) but do
not bound total accumulation across many events, so growth is linear in attacker
bandwidth — a sustained flood is required, which is why this is assessed as medium,
not high.

To Reproduce

Conceptual / code-audit finding. A relay the client connects to streams validly
signed events whose p tags reference a target public key, each with a distinct
ws(s):// hint URL (RelayUrl::parse only requires a ws/wss scheme). The target
key's relay map (or the SQLite relays_per_user rows) grows without bound.

Expected behavior

Bound the gossip store: cap relays-per-public-key, cap hints processed per event,
evict/expire stale or zero-flag entries, add a global byte/entry budget independent
of the outer public-key LRU, and enforce quota + garbage collection in the SQLite
backend. Document bounded() as the recommended constructor (or make bounded the
default) for production use.

Build environment

  • Library: nostr-gossip-memory, nostr-gossip-sqlite (via nostr-sdk gossip)
  • Version/tag/commit: 9ad7b273e7f4c460d07feea0077b3a4491dcbc6a
  • OS+version: any (source-level issue)

Additional context

  • Impact class: CWE-400 (Uncontrolled Resource Consumption) / CWE-770 (Allocation of
    Resources Without Limits). Not remotely triggerable as an instant OOM — requires a
    sustained flood from a connected relay — so severity is medium (defense-in-depth
    / resource-hardening), not critical.
  • This appears to conflict with the project's own AGENTS.md guidance to "avoid
    unbounded collections, queues, tasks, or retries."
  • Found during a security audit (second, DoS-focused pass). Cross-model verified;
    severity reconciled to medium after adversarial review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions