Account allow/block/delete endpoints skip relay ownership check (cross-tenant IDOR) #9

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

views_api.py:150-170 (PUT /api/v1/account) and views_api.py:173-187 (DELETE /api/v1/account/{relay_id}/{pubkey}) carry dependencies=[Depends(require_admin_key)] but never verify that the caller's wallet owns relay_id. api_create_or_update_account does not even receive the WalletTypeInfo; it takes data.relay_id from the body and calls get_account / create_account / update_account on the global accounts table keyed by (relay_id, pubkey). api_delete_account is the same.

Compare api_get_accounts at views_api.py:197-203, which does get_relay(wallet.wallet.user, relay_id) and 404s.

Impact: any LNbits user with any admin key can, on a relay they do not own: set allowed=true for their own pubkey and bypass pay-to-join (NostrAccount.can_join at models.py:31-33); set blocked=true for arbitrary pubkeys (enforced at relay/event_validator.py:87-91 and relay/client_connection.py:298-307), censoring other operators' users; delete other operators' account rows.

Fix direction: thread wallet: WalletTypeInfo = Depends(require_admin_key) into both handlers and gate on get_relay(wallet.wallet.user, relay_id) exactly as api_get_accounts does, before any mutation.

Found during reforge run #1 (sandbox nostrrelay#3).

`views_api.py:150-170` (`PUT /api/v1/account`) and `views_api.py:173-187` (`DELETE /api/v1/account/{relay_id}/{pubkey}`) carry `dependencies=[Depends(require_admin_key)]` but never verify that the caller's wallet owns `relay_id`. `api_create_or_update_account` does not even receive the `WalletTypeInfo`; it takes `data.relay_id` from the body and calls `get_account` / `create_account` / `update_account` on the global accounts table keyed by `(relay_id, pubkey)`. `api_delete_account` is the same. Compare `api_get_accounts` at `views_api.py:197-203`, which does `get_relay(wallet.wallet.user, relay_id)` and 404s. Impact: any LNbits user with any admin key can, on a relay they do not own: set `allowed=true` for their own pubkey and bypass pay-to-join (`NostrAccount.can_join` at `models.py:31-33`); set `blocked=true` for arbitrary pubkeys (enforced at `relay/event_validator.py:87-91` and `relay/client_connection.py:298-307`), censoring other operators' users; delete other operators' account rows. Fix direction: thread `wallet: WalletTypeInfo = Depends(require_admin_key)` into both handlers and gate on `get_relay(wallet.wallet.user, relay_id)` exactly as `api_get_accounts` does, before any mutation. Found during reforge run #1 (sandbox nostrrelay#3).
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/nostrrelay#9
No description provided.