Repository navigation
Commit 217feab
fix: revoke PUBLIC execute on all SECURITY DEFINER functions (#173)
* fix(ads): revoke PUBLIC execute on the SECURITY DEFINER ad RPCs
Phases 4-6 each ended with "revoke execute ... from anon, authenticated;
grant execute ... to service_role", which reads as service-role-only and
is not. Postgres grants EXECUTE to PUBLIC by default on every new
function, and anon/authenticated inherit it — revoking their explicit
grants leaves the PUBLIC grant untouched. The ACL stayed
"=X/postgres | postgres=X/postgres | service_role=X/postgres", where the
leading =X is PUBLIC, and has_function_privilege('anon', …) was true.
ad_charge_click is SECURITY DEFINER, so anyone holding the publishable
anon key could POST /rest/v1/rpc/ad_charge_click and forge valid clicks
against any funded campaign — draining its credits and daily budget —
while accruing publisher earnings to a slot they own, which the
self-deal guard permits because slot owner and campaign owner differ.
The ad_payouts solvency trigger capped the cash blast radius at lifetime
deposits and no payout has ever executed, so nothing was withdrawn. But
advertiser credits and every delivery metric were forgeable.
ad_apply_deposit_bonus was reachable the same way.
Revoking from PUBLIC is what actually removes it. Both callers use the
service-role client and keep their own explicit grant, so no app access
is lost. Verified in prod: anon and authenticated now false on all three,
service_role still true.
Left alone: ad_account_series / ad_campaign_totals are SECURITY INVOKER
and scope every row to owner_id = auth.uid(), so an anon caller gets an
empty result, and authenticated needs them.
Found by the Supabase security advisor
(anon_security_definer_function_executable).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: revoke PUBLIC execute on the remaining SECURITY DEFINER functions
Sweeps the same PUBLIC-grant bug as the previous commit across the other
15 SECURITY DEFINER functions in the database.
The sharpest is consume_credit(p_owner, p_count): SECURITY DEFINER, and
p_count is signed because reconcilePromo calls it with a negative count
to refund. Any holder of the publishable anon key could POST
consume_credit('<their id>', -1000000) and mint themselves credits — and
those land in credits_balance without touching promo_credits, so the ad
network would treat them as cash-backed and let them be withdrawn as
USDC, walking straight through the solvency work.
Also locked: credit_purchase_complete, consume/refund_article_*,
consume_alert_serp_budget, bump_autoblog_integration,
lx_find_internal_links, get_public_audit/get_public_findings, and the
handle_new_user / create_default_org_for_profile / rls_auto_enable
trigger functions.
Every caller was traced before revoking. worker/index.ts builds its
client from SUPABASE_SERVICE_ROLE_KEY and the route handlers and server
actions all use serviceClient(), so nothing loses access. get_public_audit
is called with svc from a server component (never the browser) and
get_public_findings has no caller at all, so the public share page is
unaffected.
Deliberately NOT revoked: the six is_org_*/is_project_*/project_owner_id
RLS helpers. They are called from inside 48 policies across 21 tables,
and a policy is evaluated as the querying role — revoking would break RLS
rather than harden it. All 48 have polroles = '{0}' (PUBLIC), so anon is
in scope too. The residual exposure is a boolean membership probe that
requires already knowing both uuids and returns no data.
Verified in prod: all 15 now anon=false authenticated=false
service_role=true, and an authenticated smoke test still resolves RLS
correctly (projects=35 campaigns=34 slots=24 audits=2344).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 72903e2 commit 217feab
2 files changed
Lines changed: 128 additions & 0 deletions
File tree
- supabase/migrations
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
0 commit comments