Skip to content

fix(db): harden security definer execute grants#1966

Merged
riderx merged 6 commits intomainfrom
codex/harden-security-definer-execute-grants
Apr 27, 2026
Merged

fix(db): harden security definer execute grants#1966
riderx merged 6 commits intomainfrom
codex/harden-security-definer-execute-grants

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 27, 2026

Summary (AI generated)

  • harden SECURITY DEFINER execute grants with a focused Supabase migration
  • restore anonymous execution only for RPCs that are intentionally reached through the API-key path
  • keep internal helpers and trigger-only functions out of direct public execution
  • add a backend grant regression test and cover the statistics/API-key flow that depends on the restored metrics RPCs

Motivation (AI generated)

Supabase flagged a large set of exposed SECURITY DEFINER functions. This PR tightens the obvious mis-grants first without breaking the existing API-key-backed product flows that still rely on a small set of privileged RPC wrappers.

Business Impact (AI generated)

This reduces the externally reachable attack surface around privileged database functions while preserving core organization and statistics flows. It lowers security risk without interrupting customer usage of API keys and dashboard statistics.

Test Plan (AI generated)

  • bun lint
  • bun run supabase:with-env -- bunx vitest run tests/security-definer-execute-hardening.test.ts tests/statistics.test.ts --reporter=verbose
  • wait for GitHub Actions on this PR and fix regressions until green

Generated with AI

Summary by CodeRabbit

  • Chores

    • Enhanced database security by hardening function access controls and enforcing least-privilege role-based permissions across different user roles.
  • Tests

    • Added validation tests to ensure database security controls and role-based access restrictions function as expected.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 31 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0547db7f-ef4d-4c4e-90d1-6b189a030bd9

📥 Commits

Reviewing files that changed from the base of the PR and between a3f37b5 and f311135.

📒 Files selected for processing (2)
  • supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql
  • tests/security-definer-execute-hardening.test.ts
📝 Walkthrough

Walkthrough

This pull request hardens database function access control by converting select helper functions to SECURITY INVOKER mode and systematically enforcing least-privilege permissions via REVOKE ALL/GRANT EXECUTE across multiple roles (PUBLIC, ANON, AUTHENTICATED, SERVICE_ROLE) for dozens of callable functions, while adding a comprehensive test suite to validate the security changes.

Changes

Cohort / File(s) Summary
Database Security Hardening
supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql
Converts 6 helper functions to SECURITY INVOKER and systematically revokes/grants execute permissions across 50+ public functions for the four main roles, ensuring least-privilege access control.
Security Validation Tests
tests/security-definer-execute-hardening.test.ts
New Vitest suite validating security-definer behavior, permission enforcement across roles, and functional correctness of helper functions via direct SQL calls to the database.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With whiskers twitching, I check each gate,
REVOKE and GRANT to seal our fate,
No anon shall peek where service reigns,
Each function tested, each security maintains!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description covers the summary and motivation well, but the test plan section is incomplete and the checklist items are not addressed as required by the template. Complete the checklist items and clarify which items apply (code style, documentation changes, E2E coverage, and manual testing steps).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: hardening security definer execute grants in the database.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/harden-security-definer-execute-grants

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented Apr 27, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/harden-security-definer-execute-grants (f311135) with main (6b257aa)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review April 27, 2026 13:50
@riderx
Copy link
Copy Markdown
Member Author

riderx commented Apr 27, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql (2)

75-77: Inconsistent REVOKE pattern—missing ANON/AUTHENTICATED revocations.

sync_org_user_role_binding_on_delete() only revokes from PUBLIC, while similar trigger/sync functions (e.g., lines 79-91) revoke from PUBLIC, ANON, and AUTHENTICATED. For consistency and defense-in-depth, consider adding the missing revocations.

Proposed fix
 REVOKE ALL
 ON FUNCTION public.sync_org_user_role_binding_on_delete()
 FROM PUBLIC;
+REVOKE ALL
+ON FUNCTION public.sync_org_user_role_binding_on_delete()
+FROM ANON;
+REVOKE ALL
+ON FUNCTION public.sync_org_user_role_binding_on_delete()
+FROM AUTHENTICATED;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql`
around lines 75 - 77, The REVOKE block for the function
sync_org_user_role_binding_on_delete() only removes privileges from PUBLIC but
should also revoke from ANON and AUTHENTICATED to match other trigger/sync
functions; update the migration to add REVOKE ALL ON FUNCTION
public.sync_org_user_role_binding_on_delete() FROM ANON; and REVOKE ALL ON
FUNCTION public.sync_org_user_role_binding_on_delete() FROM AUTHENTICATED;
(mirror the pattern used for the other sync/trigger functions in this file).

15-92: All functions in this migration are confirmed trigger functions; REVOKE statements are redundant but harmless.

Each of the 15 functions listed (including apikeys_force_server_key(), check_encrypted_bundle_on_insert(), noupdate(), sanitize_*(), sync_*(), and generate_org_user_on_org_create()) returns trigger and cannot be invoked directly by any role. The REVOKE statements provide defense-in-depth documentation of security intent and consistency across internal helper functions, though they are technically unnecessary for trigger functions per PostgreSQL semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql`
around lines 15 - 92, The REVOKE statements target functions that are trigger
functions (return type trigger) and thus cannot be called as RPCs; remove the
redundant REVOKE calls to simplify the migration: delete the REVOKE lines for
apikeys_force_server_key(), apikeys_strip_plain_key_for_hashed(),
check_encrypted_bundle_on_insert(), cleanup_onboarding_app_data_on_complete(),
generate_org_user_on_org_create() (and the entire DO $$...$$ block that revokes
it), generate_org_user_stripe_info_on_org_create(), noupdate(),
prevent_last_super_admin_binding_delete(), sanitize_apps_text_fields(),
sanitize_orgs_text_fields(), sanitize_tmp_users_text_fields(),
sanitize_users_text_fields(), sync_org_has_usage_credits_from_grants(),
sync_org_user_role_binding_on_delete(), sync_org_user_role_binding_on_update(),
and sync_org_user_to_role_binding(); leave any non-trigger-specific security
statements intact if present.
tests/security-definer-execute-hardening.test.ts (1)

101-121: Add explicit existence check for queried functions.

The LEFT JOIN returns NULL values for prosecdef, anon_exec, and auth_exec when a function doesn't exist (e.g., signature mismatch). The tests would then compare null to false/true, failing with unclear error messages rather than identifying the missing function.

Consider adding an existence assertion to catch signature mismatches early:

Proposed fix
   for (const proc of INVOKER_PROCS) {
+    expect(states.get(proc)?.prosecdef, `${proc} does not exist`).not.toBeNull()
     expect(states.get(proc)?.prosecdef, proc).toBe(false)
   }

Apply similar checks in other test cases, or add a shared helper:

function assertProcExists(states: Map<string, ProcState>, proc: string) {
  const state = states.get(proc)
  expect(state?.prosecdef, `${proc} does not exist or signature mismatch`).not.toBeNull()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/security-definer-execute-hardening.test.ts` around lines 101 - 121, The
LEFT JOIN in getProcStates returns NULL for non-existent functions causing
silent null vs boolean mismatches; add an explicit existence assertion helper
(e.g., assertProcExists(states: Map<string,ProcState>, proc: string)) that looks
up states.get(proc) and calls expect(state?.prosecdef, `${proc} does not exist
or signature mismatch`).not.toBeNull(); call this helper from the tests before
comparing anon_exec/auth_exec so missing functions fail with a clear message;
alternatively you can augment getProcStates to return an explicit exists flag,
but ensure tests first assert existence using the ProcState.prosecdef check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/security-definer-execute-hardening.test.ts`:
- Around line 22-41: Add the missing function name to the SERVICE_ONLY_PROCS
test array: include 'public.sync_org_user_role_binding_on_delete(uuid, uuid)' in
the SERVICE_ONLY_PROCS constant so the test covers the migration that revokes
execute from sync_org_user_role_binding_on_delete; update the SERVICE_ONLY_PROCS
declaration in tests/security-definer-execute-hardening.test.ts (where
SERVICE_ONLY_PROCS is defined) to contain that entry alongside the other
public.sync_* entries.

---

Nitpick comments:
In
`@supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql`:
- Around line 75-77: The REVOKE block for the function
sync_org_user_role_binding_on_delete() only removes privileges from PUBLIC but
should also revoke from ANON and AUTHENTICATED to match other trigger/sync
functions; update the migration to add REVOKE ALL ON FUNCTION
public.sync_org_user_role_binding_on_delete() FROM ANON; and REVOKE ALL ON
FUNCTION public.sync_org_user_role_binding_on_delete() FROM AUTHENTICATED;
(mirror the pattern used for the other sync/trigger functions in this file).
- Around line 15-92: The REVOKE statements target functions that are trigger
functions (return type trigger) and thus cannot be called as RPCs; remove the
redundant REVOKE calls to simplify the migration: delete the REVOKE lines for
apikeys_force_server_key(), apikeys_strip_plain_key_for_hashed(),
check_encrypted_bundle_on_insert(), cleanup_onboarding_app_data_on_complete(),
generate_org_user_on_org_create() (and the entire DO $$...$$ block that revokes
it), generate_org_user_stripe_info_on_org_create(), noupdate(),
prevent_last_super_admin_binding_delete(), sanitize_apps_text_fields(),
sanitize_orgs_text_fields(), sanitize_tmp_users_text_fields(),
sanitize_users_text_fields(), sync_org_has_usage_credits_from_grants(),
sync_org_user_role_binding_on_delete(), sync_org_user_role_binding_on_update(),
and sync_org_user_to_role_binding(); leave any non-trigger-specific security
statements intact if present.

In `@tests/security-definer-execute-hardening.test.ts`:
- Around line 101-121: The LEFT JOIN in getProcStates returns NULL for
non-existent functions causing silent null vs boolean mismatches; add an
explicit existence assertion helper (e.g., assertProcExists(states:
Map<string,ProcState>, proc: string)) that looks up states.get(proc) and calls
expect(state?.prosecdef, `${proc} does not exist or signature
mismatch`).not.toBeNull(); call this helper from the tests before comparing
anon_exec/auth_exec so missing functions fail with a clear message;
alternatively you can augment getProcStates to return an explicit exists flag,
but ensure tests first assert existence using the ProcState.prosecdef check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c301121-8477-4931-ab57-4929af6a9692

📥 Commits

Reviewing files that changed from the base of the PR and between 3095cf1 and a3f37b5.

📒 Files selected for processing (2)
  • supabase/migrations/20260427105151_harden_security_definer_execute_grants.sql
  • tests/security-definer-execute-hardening.test.ts

Comment thread tests/security-definer-execute-hardening.test.ts
@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit 9096a72 into main Apr 27, 2026
16 checks passed
@riderx riderx deleted the codex/harden-security-definer-execute-grants branch April 27, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant