Lightning-address usernames are not unique: transport create/update skip the check and the schema has no constraint #4

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

Username uniqueness is enforced only by check_username_exists (views_api.py:103-109), called only from the HTTP create/update handler (:198-207). handle_lnurlp_create (transport_rpcs.py:44-51) and handle_lnurlp_update (:86-105, "username" is in _MUTABLE) never call it. migrations.py:155 adds username TEXT with no UNIQUE index, and get_address_data (crud.py:75-79) is a fetchone with no ordering. Once two rows share a username, /.well-known/lnurlp/<name> resolves to an arbitrary one of them: a second account can register alice over the transport and receive payments addressed to alice@domain, and zap attribution for the address breaks. Even HTTP-only, check-then-insert is racy under concurrent creates.

Fix direction: call the shared validator (incl. the uniqueness check) from the transport handlers; add a fork migration creating a UNIQUE index on lnurlp.pay_links(username) (partial WHERE username IS NOT NULL on Postgres; SQLite already allows multiple NULLs) so both the transport gap and the HTTP race are closed at the schema. Test: second create with a taken username fails over both surfaces.

Found during reforge run #1 (sandbox lnurlp#10).

Username uniqueness is enforced only by `check_username_exists` (`views_api.py:103-109`), called only from the HTTP create/update handler (`:198-207`). `handle_lnurlp_create` (`transport_rpcs.py:44-51`) and `handle_lnurlp_update` (`:86-105`, `"username"` is in `_MUTABLE`) never call it. `migrations.py:155` adds `username TEXT` with no UNIQUE index, and `get_address_data` (`crud.py:75-79`) is a `fetchone` with no ordering. Once two rows share a username, `/.well-known/lnurlp/<name>` resolves to an arbitrary one of them: a second account can register `alice` over the transport and receive payments addressed to `alice@domain`, and zap attribution for the address breaks. Even HTTP-only, check-then-insert is racy under concurrent creates. Fix direction: call the shared validator (incl. the uniqueness check) from the transport handlers; add a fork migration creating a UNIQUE index on `lnurlp.pay_links(username)` (partial `WHERE username IS NOT NULL` on Postgres; SQLite already allows multiple NULLs) so both the transport gap and the HTTP race are closed at the schema. Test: second create with a taken username fails over both surfaces. Found during reforge run #1 (sandbox lnurlp#10).
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/lnurlp#4
No description provided.