fix(manager-core): F-S-013 partial unique index on app_user_invitations pending rows
No unique constraint on (app_id, lower(email)) for pending rows meant
calling users::invite("alice@…") N times created N rows with N valid
tokens. Combined with accept_invite returning Ok(None) silently when
the email already exists, an attacker who learned one token could
permanently consume it without effect.
Migration 0040 adds a partial unique index keyed on
(app_id, lower(email)) WHERE accepted_at IS NULL. Re-inviting after a
previous invite was accepted still works — the accepted row falls
outside the index.
The service-layer 409-conflict UX (the audit's secondary suggestion)
is a separate follow-up; this commit closes the data-shape hole.
AUDIT.md anchor: F-S-013.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
-- F-S-013 (audit 2026-06-07): partial unique index on pending
|
||||
-- (app_id, lower(email)) for app_user_invitations.
|
||||
--
|
||||
-- Without it, users::invite("alice@…") called N times creates N rows
|
||||
-- with N valid tokens. Combined with accept_invite returning Ok(None)
|
||||
-- silently when the email already exists, an attacker who learns one
|
||||
-- invitation token can permanently consume it without effect.
|
||||
--
|
||||
-- The constraint is partial so re-inviting after a previous invitation
|
||||
-- was accepted still works (the accepted row sits outside the index).
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_app_user_invitations_unique_pending
|
||||
ON app_user_invitations (app_id, lower(email))
|
||||
WHERE accepted_at IS NULL;
|
||||
Reference in New Issue
Block a user