Restrict invite_user_to_org RPC to authenticated callers#1710
Conversation
📝 WalkthroughWalkthroughAdds a new SECURITY DEFINER SQL function Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant DB_Function as invite_user_to_org
participant OrgsTable as Orgs
participant UsersTable as Users
participant TmpUsers as tmp_users
Caller->>DB_Function: CALL invite_user_to_org(email, org_id, invite_type)
DB_Function->>OrgsTable: validate org exists
DB_Function->>DB_Function: resolve caller identity & rights (RBAC / super-admin)
alt super-admin and 2FA required
DB_Function->>Caller: require 2FA (verify)
end
DB_Function->>UsersTable: check existing user by email
alt user exists
DB_Function->>DB_Function: create org membership/invite -> return OK or ALREADY_INVITED
else user does not exist
DB_Function->>TmpUsers: check/create pending invite, enforce recent-cancel rules
DB_Function->>DB_Function: return OK / ALREADY_INVITED / TOO_RECENT_INVITATION_CANCELATION / NO_EMAIL
end
DB_Function->>Caller: return status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 SQLFluff (4.0.4)supabase/migrations/20260227150000_fix_invite_user_to_org_security.sqlUser Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects: 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c7b781740
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| REVOKE EXECUTE ON FUNCTION public.invite_user_to_org( | ||
| character varying, | ||
| uuid, | ||
| public.user_min_right | ||
| ) FROM "anon"; |
There was a problem hiding this comment.
Reinstate anon execute on invite_user_to_org
/organization/members still calls invite_user_to_org for non-RBAC orgs (supabase/functions/_backend/public/organization/members/post.ts, legacy branch) through supabaseApikey, which authenticates with the anon key plus capgkey header (supabase/functions/_backend/utils/supabase.ts). Revoking anon execute here causes those valid API-key requests to fail with function-permission errors, so member invites break for legacy orgs; keep anon execute and rely on the function’s internal get_identity_org_allowed/check_min_rights checks to reject unauthenticated callers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql`:
- Around line 62-63: The 2FA check and audit log are using auth.uid() but
authorization earlier uses calling_user_id from get_identity_org_allowed(), so
update the IF and pg_log call in invite_user_to_org to use calling_user_id
instead of auth.uid(): call public.has_2fa_enabled(calling_user_id) in the IF
condition when org.enforcing_2fa is true, and include 'uid': calling_user_id in
the jsonb_build_object passed to public.pg_log so the correct resolved caller is
checked and recorded; keep all other logic and names (invite_user_to_org,
org.enforcing_2fa, public.has_2fa_enabled, public.pg_log) unchanged.
- Around line 119-135: The function public.invite_user_to_org currently revokes
EXECUTE only from "anon" but inherits EXECUTE from PUBLIC; before the existing
REVOKE on "anon" add an explicit REVOKE EXECUTE ON FUNCTION
public.invite_user_to_org(character varying, uuid, public.user_min_right) FROM
PUBLIC so the PUBLIC grant is removed first, then keep the existing REVOKE FROM
"anon" and the subsequent GRANTs to "authenticated" and "service_role"; adjust
the ACL order around the invite_user_to_org function to ensure PUBLIC cannot
execute it after CREATE OR REPLACE.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql`:
- Around line 125-129: The GRANT that gives EXECUTE on the SECURITY DEFINER RPC
invite_user_to_org to the "anon" role must be removed and replaced with a grant
to the authenticated role; locate the GRANT EXECUTE ON FUNCTION
public.invite_user_to_org(...) TO "anon" statement and delete it (or change the
grantee) so that only the authenticated role (not "anon") is granted EXECUTE,
ensuring the RPC can only be called by JWT-authenticated clients.
| GRANT EXECUTE ON FUNCTION public.invite_user_to_org( | ||
| character varying, | ||
| uuid, | ||
| public.user_min_right | ||
| ) TO "anon"; |
There was a problem hiding this comment.
Remove anon EXECUTE grant to match the security objective.
Line 125 re-grants EXECUTE to "anon", which reopens unauthenticated access to this SECURITY DEFINER RPC and conflicts with the stated goal of authenticated-only invocation.
Proposed fix
-GRANT EXECUTE ON FUNCTION public.invite_user_to_org(
+REVOKE EXECUTE ON FUNCTION public.invite_user_to_org(
character varying,
uuid,
public.user_min_right
-) TO "anon";
+) FROM "anon";Based on learnings: API keys authenticate through the authenticated role via JWT, not the anon role.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql`
around lines 125 - 129, The GRANT that gives EXECUTE on the SECURITY DEFINER RPC
invite_user_to_org to the "anon" role must be removed and replaced with a grant
to the authenticated role; locate the GRANT EXECUTE ON FUNCTION
public.invite_user_to_org(...) TO "anon" statement and delete it (or change the
grantee) so that only the authenticated role (not "anon") is granted EXECUTE,
ensuring the RPC can only be called by JWT-authenticated clients.
|
* fix(api): preserve channel owner on channel upsert * fix(auth): block sensitive account actions for unverified users (#1690) * fix(auth): block account deletion for unverified users * fix(auth): refresh session fields for email verification gate * fix(auth): make delete_user insert idempotent * fix(auth): explain blocked delete/settings when email unverified * fix(auth): block delete action when email is unverified * fix(auth): localize resend email block and make delete_user idempotent * Restrict invite_user_to_org RPC to authenticated callers (#1710) * fix(db): restrict invite_user_to_org public rpc * fix(db): use caller identity in invite 2FA check * fix(security): restrict webhook select to admin users (#1705) * Secure record_build_time RPC for authorized callers (#1711) * fix(db): secure record_build_time rpc writes * fix(db): preserve service-role record_build_time path * fix(api): preserve channel owner on channel upsert
* fix(security): restrict apikey oracle rpc access * fix: webapp url * fix: fix * chore(release): 12.116.9 * fix: envs * Revert "Merge pull request #1707 from Cap-go/fix_webapp_url" This reverts commit ff20d1a. * fix: typo * chore(release): 12.116.10 * fix(security): restrict apikey oracle rpc access * fix: return 503 instead of 400 for service_unavailable build errors Builder availability errors (not configured, call failed, error response, missing upload URL) are transient server-side failures, not client errors. Returning 503 allows the CLI retry logic to automatically retry these requests instead of treating them as terminal 400 errors. * chore(release): 12.116.11 * fix: update PWD script * fix: env vars * fix: modal responsive * feat: forward buildOptions + buildCredentials to builder (pass-through) * fix: correct vue/html-indent in DemoOnboardingModal * fix: use snake_case (build_options, build_credentials) in public API, map to camelCase for builder * fix(security): sanitize SQL interpolation in Cloudflare Analytics Engine queries (#1702) * chore(release): 12.116.12 * Add unit tests for builder payload shape contract Extract buildBuilderPayload() from the inline fetch body so the snake_case → camelCase mapping and exact key set can be tested. 6 vitest cases verify: camelCase output, no legacy credentials field, correct metadata keys, and pass-through of contents. * Reject deprecated `credentials` field with clear upgrade error Old CLI clients sending the flat `credentials` field would have it silently dropped, causing confusing builder failures. Now the proxy explicitly rejects non-empty `credentials` with a migration message pointing to `build_credentials`. * fix(security): clean up role_bindings on member removal (#1722) * chore(release): 12.116.13 * fix(security): use parameterized query in getStoreAppByIdCF to prevent SQL injection The appId parameter was directly interpolated into the D1 SQL query string, creating a SQL injection vulnerability. Switched to bound parameter via .bind(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): prevent privilege escalation in role_bindings endpoint Add priority_rank check so callers cannot assign or update roles with higher privileges than their own. Without this, any user with org.update_user_roles could escalate to org_super_admin. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): enforce is_assignable in role_bindings INSERT RLS policy Direct PostgREST inserts could bypass the endpoint's is_assignable check and assign non-assignable roles (e.g. platform_super_admin). The RLS INSERT policy now requires the target role to have is_assignable = true. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): cascade all role bindings on member removal delete_org_member_role previously only deleted the org-level binding, leaving orphaned app/channel bindings. A removed member could retain app-level access. Now deletes all bindings for the user in the org. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): add trigger to prevent deleting last super_admin binding Direct PostgREST DELETEs on role_bindings could bypass the last super_admin guards in delete_org_member_role. A BEFORE DELETE trigger now rejects deletion of the last org_super_admin binding in any org. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): support hashed API keys in rbac_check_permission_direct The RBAC path in rbac_check_permission_direct looked up API keys with WHERE key = p_apikey, which silently failed for hashed keys. Switched to find_apikey_by_value() which handles both plain-text and hashed keys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: reword comment to pass typos CI check Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove unused desc import from role_bindings.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(security): add FOR UPDATE lock to prevent write-skew on last super_admin delete Two concurrent DELETE transactions could both pass the count check and both delete their rows, leaving zero super_admins. A SELECT ... FOR UPDATE on the super_admin binding set now serializes concurrent deletes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prevent API key privilege escalation and fix organization member deletion test - Add validation to prevent limited API keys from creating unlimited keys - Fix organization-api test to work with sync_org_user_to_role_binding trigger - Change test user_right from 'invite_read' to 'read' (trigger-compatible) - Verify trigger-created role_bindings instead of manually inserting them * fix: allow CASCADE deletions in prevent_last_super_admin_binding_delete and fix RBAC test compatibility - Add org existence check in trigger to allow CASCADE deletions when org is being deleted - Add service_role bypass for administrative operations and tests - Update tests to work with RBAC security constraints: - 34_test_rbac_rls.sql: Remove DELETE operation that violated super_admin protection - 35_test_is_admin_rbac.sql: Use service_role for test setup INSERT - All SQL database tests now pass (860 tests) - Backend tests remain passing (68 tests) * fix(security): make getCallerMaxPriorityRank auth-type-aware and remove API key data leak * chore(release): 12.116.14 * fix(security): correct API key RBAC principal mapping and remove service_role bypass * fix(security): correct RBAC migration comments and add privilege check on delete - Update migration comments to accurately reflect that service_role is NOT exempt from the last super_admin protection trigger - Replace FOR UPDATE scan with pg_advisory_xact_lock to avoid cross-transaction deadlocks - Add privilege-rank check in delete handler to prevent deleting higher-ranked role bindings - Aligns with established advisory lock patterns in codebase Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix: add self-2fa-required message for 2FA enforcement in multiple languages * chore(release): 12.116.15 * fix(frontend): validate 2fa before enabling org enforcement (#1729) * chore(release): 12.116.16 * fix(deps): update vue monorepo to v3.5.29 (#1731) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(release): 12.116.17 * chore: remove unused cloudflare function getStoreAppByIdCF * chore(release): 12.116.18 * chore: stop editing immutable base migration * fix(frontend): disable auto demo onboarding modal (#1733) * chore(release): 12.116.19 * fix(auth): block sensitive account actions for unverified users (#1690) * fix(auth): block account deletion for unverified users * fix(auth): refresh session fields for email verification gate * fix(auth): make delete_user insert idempotent * fix(auth): explain blocked delete/settings when email unverified * fix(auth): block delete action when email is unverified * fix(auth): localize resend email block and make delete_user idempotent * Restrict invite_user_to_org RPC to authenticated callers (#1710) * fix(db): restrict invite_user_to_org public rpc * fix(db): use caller identity in invite 2FA check * fix(security): restrict webhook select to admin users (#1705) * Secure record_build_time RPC for authorized callers (#1711) * fix(db): secure record_build_time rpc writes * fix(db): preserve service-role record_build_time path * fix(security): restrict apikey oracle rpc access * chore: stop editing immutable base migration * fix(security): restrict apikey oracle rpc access * chore(release): 12.116.20 * fix(security): restrict apikey oracle rpc access * chore: stop editing immutable base migration * fix(security): restrict apikey oracle rpc access * chore: stop editing immutable base migration * fix(security): restrict apikey oracle rpc access --------- Co-authored-by: WcaleNieWolny <isupermichael007@gmail.com> Co-authored-by: WcaleNieWolny <50914789+WcaleNieWolny@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: LOLO <131777939+artylobos@users.noreply.github.com> Co-authored-by: Jordan Lorho <jordan.lorho@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>



Summary (AI generated)
public.invite_user_to_orgto remove org-id enumeration and keep invitation status handling intact for existing callers.calling_user_id.PUBLICaccess while keeping legacy API-key flows compatible through explicitanonexecution rights.Motivation (AI generated)
anonexecution while still exposing the function toPUBLIC, and usedauth.uid()in 2FA checks where authenticated API-key flows resolve identity differently./organization/membersand related legacy flows use API-key-authenticatedsupabaseApikeyclients on theanonrole, so fully revokinganonwould break legitimate invitations.Business Impact (AI generated)
Test Plan (AI generated)
bun lint:backend./organization/memberslegacy org path (rbac=false) with a valid API key./organization/memberswith a non-existent org ID and verifyNO_RIGHTSresponse.Summary by CodeRabbit