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:573 — update_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:57 — NostrGossipMemory::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.
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 everyvalid event's
ptags and inserts each attacker-supplied relay hint into thereferenced public key's
PkData.relays: IndexMap(struct at line 34-37) viaupdate_relay_per_user(lines 377-394). There is no per-key cap or eviction. TheNIP-65 and NIP-17 arms are capped (
.take(MAX_NIP65_SIZE)/.take(MAX_NIP17_SIZE),= 7atconstant.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 thenumber 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:573—update_hints(called fromprocess_event, line 112) inserts intopublic_keys,relays, andrelays_per_userwith 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:57—NostrGossipMemory::unbounded()builds
LruCache::unbounded(). It is an explicit opt-in (there is noDefault;bounded(limit)exists), so this is lower severity, but neither the docs nor theexamples mark
bounded()as the recommended production path.Reachability.
nostr-sdk/src/relay/inner.rs:1322-1325callsgossip.process(&event, ...)for every verified event received from any connectedrelay. 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
ptags reference a target public key, each with a distinctws(s)://hint URL (RelayUrl::parseonly requires a ws/wss scheme). The targetkey's relay map (or the SQLite
relays_per_userrows) 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 thedefault) for production use.
Build environment
9ad7b273e7f4c460d07feea0077b3a4491dcbc6aAdditional context
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.
unbounded collections, queues, tasks, or retries."
severity reconciled to medium after adversarial review.