Repository navigation
Conversation
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 43 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Supabase Auth limits requests per client address only, so one address could try about 1,800 passwords an hour and many addresses could try one account without limit. The proxy now counts the requests that check a secret someone could guess before it forwards them: password sign-in (every /token grant but refresh_token, pkce and id_token, with the grant read from the query and from the body as a form, as Go's FormValue does), POST /verify naming an email or phone, and POST /factors/<id>/verify. Each takes a slot from three token buckets: the client address in any 15 minutes (AUTH_GUESS_LIMIT_PER_CLIENT, 30), the account from that address in any 15 minutes (AUTH_GUESS_LIMIT_PER_ACCOUNT_CLIENT, 10) and the account from every address in an hour (AUTH_GUESS_LIMIT_PER_ACCOUNT, 50). Attempts Supabase Auth accepts, or refuses with its own 429, give their slots back. Over a budget: 429 RATE_LIMITED with Retry-After and error_code over_request_rate_limit, which the SPA now shows. The kernel refuses to start when one address could drain an account's budget. The counts live in memory per API process (no database write for unauthenticated traffic), with hashed account keys and a bounded number of buckets. Plan §4.4 step 0 and §4.11 record the decision per path. Closes #221 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ
133068d to
b4c5594
Compare
Closes #221
Problem
Supabase Auth (v2.197.0, as pinned) limits requests per client address only, keyed on the
X-Real-IPthe proxy sets./tokenallows a burst of 30, then 150 per 5 minutes./verifyallows 30 per 5 minutes. MFA challenge and verify allow 15 a minute. So one address could try about 1,800 passwords an hour, and many addresses could try one account, or one 6-digit emailed or TOTP code, without any limit. #190 (#217) covers only the endpoints that send email. #213's per-user limit applies only once a token exists.Decision per path (recorded in plan §4.11)
POST /token, password grantPOST /verifynaming an email or phone (6-digit code)POST /factors/<id>/verify(TOTP code)/tokenwithrefresh_token,pkce,id_tokenPOST /verifywith a link's token hash onlyotpuserEvery
/tokengrant except the three safe ones counts as a password grant. The grant is read from the query and also from the body parsed as a form, whatever the Content-Type, because Go'sFormValueprefers a form body'sgrant_type. Without that, a request saying?grant_type=refresh_tokencould carry a password guess past the count. A multipart body is never treated as safe.Budgets
Each counted attempt takes a slot from three in-memory token buckets before it is forwarded:
AUTH_GUESS_LIMIT_PER_CLIENT(default 30): per client address (IPv6 per /64), all accounts together, in any 15 minutes.AUTH_GUESS_LIMIT_PER_ACCOUNT_CLIENT(default 10): per account from one address in any 15 minutes.AUTH_GUESS_LIMIT_PER_ACCOUNT(default 50): per account from every address together in an hour.An attempt Supabase Auth accepts (2xx), or refuses with its own 429 (so the secret was never checked), gives its slots back. In effect only failed attempts count. A refusal keeps the slots, and so does no answer, since the secret may have been checked. Over a budget the proxy forwards nothing and answers 429 with
code: RATE_LIMITED,Retry-Afteranderror_code: over_request_rate_limit. Thaterror_codeis Supabase Auth's own, which auth-js reads, and the SPA now shows a message for it in vi and en.Lockout. The kernel refuses to start when the per-account budget is less than an hour of what one address may try on one account (4 ×
AUTH_GUESS_LIMIT_PER_ACCOUNT_CLIENT). One address alone therefore never drains an account's budget. A test runs six hours of full-rate attempts from one address and checks that the owner always gets through. Locking someone out takes several addresses, and even then they can sign in with an emailed link (recovery or magic link). Links and PKCE code exchanges are not counted.Why in memory. Plan §4.4 step 0 asks for limits before any DB write, so unauthenticated traffic should not cost a database write per attempt. As with #213's per-user
/apilimit, the budgets hold per API instance: N instances allow N times each budget, as documented. Account keys are SHA-256 hashes, so a 1 MB email value cannot bloat memory. Each budget keeps at most 50,000 buckets. A full map first drops buckets that have refilled, then the bucket touched longest ago. It is swept at most once a second, so a flood of new keys cannot force a full sweep on every request.Residual risks (plan §4.11): an attacker with many addresses (a botnet, many IPv6 /64s) gets many per-address budgets and can keep one account out of password sign-in. A restart, or a flood of other accounts that evicts an account's bucket, gives that account a fresh budget.
Changes
apps/kernel/src/auth-guess-limit.ts(new):guessRequestdecides which requests count and under which account;TokenBucketsandcreateGuessLimiterhold the buckets. It does not reuse feat(kernel): bound refusal audits and rate-limit requests per actor #213'sRateLimiter(rate-limit.ts). This one needs slots given back, an arbitrary window, and eviction rather than refusing new keys when full: its keys come from unauthenticated clients, and refusing every new key would let a flood of addresses lock everyone out.apps/kernel/src/auth-email-limit.ts:bodyAccount,fieldValuesandjsonFieldsare exported, so both limiters read email and phone keys the same way (any case; conflicting values give no account). Behavior is unchanged and the existing tests pass.apps/kernel/src/http.ts: reserves slots before forwarding, gives them back on 2xx or 429. The untrusted-proxy warning now also mentions the sign-in attempt budget.apps/kernel/src/config.ts: the three settings and the lockout guard.docker-compose.yml,.env.exampleandREADME.mdlist them.apps/app:over_request_rate_limitinAUTH_ERRORS, with vi and en messages.Note for the e2e stack
The e2e stack (#91, #101, #191) is not on
mainyet. #191's spec makes more than 30 failed password sign-ins from one runner address to reach Supabase Auth's own limit. Once both land,e2e/compose.e2e.ymlneeds a higherAUTH_GUESS_LIMIT_PER_CLIENT, the same way it already raises the email budget. I'll add that to whichever PR merges second.Tests
apps/kernel/test/auth-guess-limit.test.ts(26 tests):Mutation checks: reading the grant from the query only, dropping the refund on accepted attempts, and removing the sweep throttle each make a test fail.
Rebased on
mainat e75e6b2 (after #213, #198, #248), all on PostgreSQL 17:pnpm lint,pnpm typecheck,pnpm docs:checkpnpm testpnpm test:dbvitest --config vitest.invariant.config.tsSecurity review (session, while #95 is pending)
http.tsandauth*.tsare protected paths, so I reviewed the change myself:The one finding was that a full map could be made to sweep on every request. It is fixed here by the once-a-second throttle, which has a test. No migration, no database change, no invariant test touched.
🤖 Generated with Claude Code
https://claude.ai/code/session_013LrvuThKGaPX1uDwUiwmGQ