NostrRouter buffers are ClassVars: unbounded growth and isolation-by-luck between connections #8

Open
opened 2026-10-09 17:11:54 +00:00 by padreug · 0 comments
Owner

router.py:39-44 declares received_subscription_events, received_subscription_notices, received_subscription_eosenotices and received_command_results as ClassVars shared across every websocket connection. The single subscribe_events thread writes into them for any sub_id the relays echo (tasks.py:41-42,50,58-61); each connection drains only the keys in its own original_subscription_ids (router.py:109-114).

Problems:

  • Isolation between connections rests entirely on urlsafe_short_hash() uniqueness of the rewritten sub-id; nothing structural prevents one connection draining another's events.
  • stop() (router.py:62-78) closes subscriptions and clears received_command_results for pending publishes, but never deletes this connection's keys from received_subscription_events / received_subscription_eosenotices. Events arriving after disconnect, or for ids no live connection owns, are stored forever.
  • received_subscription_notices (router.py:185-191) is popped by whichever connection runs first and then discarded; with zero connections it only grows.

PR #3 already introduced the right pattern for OKs: tasks.py:63-72 only keeps results some router is waiting on, and router.py:65-66 clears them on stop. The other three buffers need the same treatment.

Impact: memory exhaustion reachable from the unauthenticated public endpoint; latent cross-connection event leak on the endpoint labelled private.

Fix direction: make the buffers per-instance (like original_subscription_ids), have the relay-manager callbacks route to the owning router via a registry keyed by rewritten sub-id (drop unknown ids on the floor), delete this connection's keys in stop(), and add a hard cap/TTL per subscription buffer. Check against real #4 (nostr-sdk rebase) since the callbacks in tasks.py are rewritten there.

Found during reforge run #1 (sandbox nostrclient#5).

`router.py:39-44` declares `received_subscription_events`, `received_subscription_notices`, `received_subscription_eosenotices` and `received_command_results` as `ClassVar`s shared across every websocket connection. The single `subscribe_events` thread writes into them for any `sub_id` the relays echo (`tasks.py:41-42,50,58-61`); each connection drains only the keys in its own `original_subscription_ids` (`router.py:109-114`). Problems: - Isolation between connections rests entirely on `urlsafe_short_hash()` uniqueness of the rewritten sub-id; nothing structural prevents one connection draining another's events. - `stop()` (`router.py:62-78`) closes subscriptions and clears `received_command_results` for pending publishes, but never deletes this connection's keys from `received_subscription_events` / `received_subscription_eosenotices`. Events arriving after disconnect, or for ids no live connection owns, are stored forever. - `received_subscription_notices` (`router.py:185-191`) is popped by whichever connection runs first and then discarded; with zero connections it only grows. PR #3 already introduced the right pattern for OKs: `tasks.py:63-72` only keeps results some router is waiting on, and `router.py:65-66` clears them on stop. The other three buffers need the same treatment. Impact: memory exhaustion reachable from the unauthenticated public endpoint; latent cross-connection event leak on the endpoint labelled private. Fix direction: make the buffers per-instance (like `original_subscription_ids`), have the relay-manager callbacks route to the owning router via a registry keyed by rewritten sub-id (drop unknown ids on the floor), delete this connection's keys in `stop()`, and add a hard cap/TTL per subscription buffer. Check against real #4 (nostr-sdk rebase) since the callbacks in `tasks.py` are rewritten there. Found during reforge run #1 (sandbox nostrclient#5).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/nostrclient#8
No description provided.