The finding
Every row in auth_audit_logs has ip_address = null and user_agent = null on the
paths that matter. Across 7,665 rows:
sign_in_failed 4172 ip_address: all null
password_reset_request 934 ip_address: all null
sign_up 103 ip_address: all null, user_agent: all null
The user_agent column is populated on sign_in_failed (4,113 of 4,172 share one
Windows/Chrome string), but it is null on sign_up, and ip_address is null
everywhere.
Why it matters now
#608 documents 5,106 junk auth calls against production. There is nothing in this
table to block on. No IP, so no per-origin throttle, no ban list, no ASN analysis,
no way to tell one attacker from many. The audit trail records that we were attacked
and nothing about who.
This is not hypothetical bookkeeping — it is the direct blocker on fixing #608
properly. Any rate limit built today can only key on email or on a global counter,
both of which an attacker rotating addresses walks straight past.
Scope
Whatever writes these rows needs the request IP threaded through. Supabase Edge
Functions receive it on x-forwarded-for; a trigger firing inside Postgres does not
have it and cannot get it, so if the write happens in-database the capture point has
to move.
Acceptance
- A failed sign-in from a known origin writes a row whose
ip_address matches that
origin. Assert the value, not that the column is non-null — a hardcoded
'0.0.0.0' would satisfy a null check and still be useless.
sign_up rows carry user_agent like sign_in_failed already does.
The finding
Every row in
auth_audit_logshasip_address = nullanduser_agent = nullon thepaths that matter. Across 7,665 rows:
The
user_agentcolumn is populated onsign_in_failed(4,113 of 4,172 share oneWindows/Chrome string), but it is null on
sign_up, andip_addressis nulleverywhere.
Why it matters now
#608 documents 5,106 junk auth calls against production. There is nothing in this
table to block on. No IP, so no per-origin throttle, no ban list, no ASN analysis,
no way to tell one attacker from many. The audit trail records that we were attacked
and nothing about who.
This is not hypothetical bookkeeping — it is the direct blocker on fixing #608
properly. Any rate limit built today can only key on email or on a global counter,
both of which an attacker rotating addresses walks straight past.
Scope
Whatever writes these rows needs the request IP threaded through. Supabase Edge
Functions receive it on
x-forwarded-for; a trigger firing inside Postgres does nothave it and cannot get it, so if the write happens in-database the capture point has
to move.
Acceptance
ip_addressmatches thatorigin. Assert the value, not that the column is non-null — a hardcoded
'0.0.0.0'would satisfy a null check and still be useless.sign_uprows carryuser_agentlikesign_in_failedalready does.