Inbound Nostr sync accepts calendar events from any pubkey: takeover of local events and unmoderated catalog injection #71

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

wait_for_nostr_events subscribes to every kind 31922/31923 on the relays (nostr_sync.py:140-144) and _handle_calendar_event upserts whatever arrives. Correlation is by d tag, and our own published events use d = event.id, so the id of any approved event is public. If a row matches and the incoming created_at is newer, the row's name, info, dates, banner, location and categories are overwritten (nostr_sync.py:93-101) with no comparison of the incoming pubkey against the signer the publisher used for that event. If nothing matches, a new row is inserted with wallet="" and status="approved" (nostr_sync.py:108-118), bypassing the proposed/approved workflow and landing straight in GET /api/v1/events/public. An attacker can keep bumping created_at to re-win against legitimate republishes. The task runs on every start (__init__.py:126).

Fix direction: only apply an update when the incoming pubkey equals the pubkey recorded for the event's owner wallet signer (store it on the row at publish time); never insert discovered events as approved (a separate discovered status or table, or drop them); scope the REQ with an authors filter to known signer pubkeys. The sync feature was added in aiolabs/events#5/#15 with no trust model; this is the inbound counterpart of the outbound custody hardening in api_event_update.

Found during reforge run #1 (sandbox events#9).

`wait_for_nostr_events` subscribes to every kind 31922/31923 on the relays (`nostr_sync.py:140-144`) and `_handle_calendar_event` upserts whatever arrives. Correlation is by `d` tag, and our own published events use `d = event.id`, so the id of any approved event is public. If a row matches and the incoming `created_at` is newer, the row's name, info, dates, banner, location and categories are overwritten (`nostr_sync.py:93-101`) with no comparison of the incoming `pubkey` against the signer the publisher used for that event. If nothing matches, a new row is inserted with `wallet=""` and `status="approved"` (`nostr_sync.py:108-118`), bypassing the proposed/approved workflow and landing straight in `GET /api/v1/events/public`. An attacker can keep bumping `created_at` to re-win against legitimate republishes. The task runs on every start (`__init__.py:126`). Fix direction: only apply an update when the incoming `pubkey` equals the pubkey recorded for the event's owner wallet signer (store it on the row at publish time); never insert discovered events as `approved` (a separate `discovered` status or table, or drop them); scope the REQ with an `authors` filter to known signer pubkeys. The sync feature was added in aiolabs/events#5/#15 with no trust model; this is the inbound counterpart of the outbound custody hardening in `api_event_update`. Found during reforge run #1 (sandbox events#9).
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/events#71
No description provided.