Skip to content

fix: harden remaining helper RPC authz#1810

Merged
riderx merged 2 commits into
mainfrom
codex/fix-ghsa-hc74-rpc-authz-automation
Mar 18, 2026
Merged

fix: harden remaining helper RPC authz#1810
riderx merged 2 commits into
mainfrom
codex/fix-ghsa-hc74-rpc-authz-automation

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 17, 2026

Summary (AI generated)

  • harden remaining helper RPCs that still allowed cross-tenant billing, storage, and membership disclosure
  • revoke anonymous execution on self-only helpers and return non-disclosing values for unauthorized org-scoped calls
  • add pgTAP coverage for authorized, cross-tenant, and anonymous access patterns

Motivation (AI generated)

The oldest open security advisory in triage identified helper RPCs that still exposed org billing/storage state and user relationship signals without sufficient caller-aware authorization. This closes that remaining surface using the same authz pattern already adopted by earlier hardening migrations.

Business Impact (AI generated)

This reduces security risk around tenant isolation and billing metadata exposure, and it resolves an externally reported advisory with a validated patch. It also lowers the chance of follow-on exploit chains built from org-state and membership oracles.

Test Plan (AI generated)

  • bun run supabase:db:reset
  • node_modules/supabase/bin/supabase test db supabase/tests/00-supabase_test_helpers.sql supabase/tests/47_test_helper_rpc_authz.sql --workdir .context/supabase-worktrees/b74947c2
  • bun run supabase:with-env -- bunx vitest run tests/upsert-version-meta-rpc.test.ts tests/build_time_tracking.test.ts tests/get-identity-apikey-only-rpc.test.ts
  • bun run test:backend

Generated with AI

Summary by CodeRabbit

  • Bug Fixes

    • Hardened helper RPCs with stricter, caller-aware authorization to prevent unauthorized access to organizational and account data.
  • Tests

    • Added comprehensive tests validating authorization behavior across admin, non-admin, and anonymous contexts, and ensured authenticated test flows for plan-related checks.

@riderx riderx added the codex label Mar 17, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04f3143b-3311-4e64-85c3-f0b5a46c409e

📥 Commits

Reviewing files that changed from the base of the PR and between bde227a and 8504e19.

📒 Files selected for processing (1)
  • supabase/tests/08_plan_functions.sql

📝 Walkthrough

Walkthrough

A migration hardens 11 public helper RPCs by removing anonymous/public EXECUTE, adding SECURITY DEFINER or STABLE definitions, and embedding caller-aware identity and rights checks (via get_identity_org_allowed / check_min_rights or session_user checks) before executing org/payment/storage/member lookup logic. Tests exercising admin, user, and anonymous behaviors were added.

Changes

Cohort / File(s) Summary
Security-Hardened Helper RPCs Migration
supabase/migrations/20260317020451_secure_remaining_helper_rpcs.sql
Creates/replaces 11 helper RPCs (is_canceled_org, is_good_plan_v5_org, is_onboarded_org, is_onboarding_needed_org, is_org_yearly, is_paying_and_good_plan_org, get_total_storage_size_org, get_total_app_storage_size_orgs, get_user_main_org_id, is_member_of_org, is_account_disabled) with SECURITY DEFINER/STABLE, revokes public/anon EXECUTE, grants EXECUTE to appropriate roles, and enforces caller-aware identity/rights checks before performing org/payment/storage/member queries.
RPC Authorization Tests
supabase/tests/47_test_helper_rpc_authz.sql
Adds a 15-test script validating RPC behavior across admin, non-admin, and anonymous contexts (checks return values, membership restrictions, blocked anon execution on sensitive RPCs, and account-disabled logic via service role injection).
Test Context Adjustment
supabase/tests/08_plan_functions.sql
Wraps existing plan tests with explicit tests.authenticate_as('test_admin') / tests.clear_authentication() calls to ensure correct auth context for is_good_plan_v5_org assertions.

Sequence Diagram

sequenceDiagram
    actor Client
    participant RPC as Helper RPC (SECURITY DEFINER)
    participant Identity as get_identity_org_allowed
    participant Rights as check_min_rights
    participant DB as Database

    Client->>RPC: Call helper (e.g., is_paying_and_good_plan_org(org_id))
    RPC->>RPC: Read caller context (current_setting('role') / session_user)
    RPC->>Identity: Resolve identity / membership for caller & org
    Identity->>DB: Query org, user_org_members, stripe_info, plans
    DB-->>Identity: Identity record / org data
    Identity-->>RPC: Identity result
    RPC->>Rights: Check minimum rights for requested operation
    Rights->>DB: Validate membership/role/ownership
    DB-->>Rights: Rights decision
    Rights-->>RPC: Authorization pass/fail
    alt Authorization Passes
        RPC->>DB: Run main logic (plans, dates, usage, storage sums)
        DB-->>RPC: Result (boolean / numeric)
        RPC-->>Client: Return result
    else Authorization Fails
        RPC-->>Client: NULL / access denied
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

Poem

🐰 I hopped through SQL, audits in my paw,

I wrapped each helper in a guarded law.
No curious anon can rummage or pry,
Admins and services still pass by.
A tidy migration — secure and spry! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: hardening authorization on remaining helper RPC functions to prevent cross-tenant disclosure.
Description check ✅ Passed The description includes a summary of changes, test plan with verification checkmarks, and addresses business impact and security motivation, covering all critical sections of the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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/fix-ghsa-hc74-rpc-authz-automation
📝 Coding Plan
  • Generate coding plan for human review comments

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/tests/08_plan_functions.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


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

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bde227a450

ℹ️ 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".

Comment on lines +512 to +516
SELECT public.get_identity_org_allowed('{read,upload,write,all}'::public.key_mode[], is_member_of_org.org_id)
INTO caller_id;

IF caller_id IS NULL OR caller_id <> is_member_of_org.user_id OR NOT public.check_min_rights(
'read'::public.user_min_right,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve parameter-based API key membership checks

is_member_of_org now derives the caller exclusively from get_identity_org_allowed(...) and rejects when that identity does not match user_id, which breaks existing anon RPC flows that pass API keys as function arguments instead of the capgkey header. In particular, public.get_org_owner_id(apikey, app_id) still computes real_user_id from its apikey parameter and calls is_member_of_org(real_user_id, org_id); for anonymous PostgREST requests without a capgkey header, this new guard returns false even for valid keys, causing NO_RIGHTS for previously valid calls.

Useful? React with 👍 / 👎.

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.

🧹 Nitpick comments (2)
supabase/migrations/20260317020451_secure_remaining_helper_rpcs.sql (1)

447-452: Consider no-argument client wrappers for self-only RPCs.

get_user_main_org_id(uuid) and is_account_disabled(uuid) are self-only by behavior, but their client-facing signatures still accept arbitrary uuid input. A () wrapper for client usage (internally using auth.uid()) plus a service-only uuid overload tightens API boundaries and removes unnecessary caller-controlled parameters.

♻️ Suggested pattern
- GRANT EXECUTE ON FUNCTION "public"."get_user_main_org_id"("user_id" "uuid") TO "authenticated";
+ GRANT EXECUTE ON FUNCTION "public"."get_user_main_org_id"() TO "authenticated";
+ GRANT EXECUTE ON FUNCTION "public"."get_user_main_org_id"("user_id" "uuid") TO "service_role";

+ CREATE OR REPLACE FUNCTION "public"."get_user_main_org_id"() RETURNS "uuid"
+   LANGUAGE "plpgsql" SECURITY DEFINER
+   SET "search_path" TO ''
+ AS $$
+ BEGIN
+   RETURN public.get_user_main_org_id(auth.uid());
+ END;
+ $$;

- GRANT EXECUTE ON FUNCTION "public"."is_account_disabled"("user_id" "uuid") TO "authenticated";
+ GRANT EXECUTE ON FUNCTION "public"."is_account_disabled"() TO "authenticated";
+ GRANT EXECUTE ON FUNCTION "public"."is_account_disabled"("user_id" "uuid") TO "service_role";

+ CREATE OR REPLACE FUNCTION "public"."is_account_disabled"() RETURNS boolean
+   LANGUAGE "plpgsql" SECURITY DEFINER
+   SET "search_path" TO ''
+ AS $$
+ BEGIN
+   RETURN public.is_account_disabled(auth.uid());
+ END;
+ $$;

As per coding guidelines: "For PostgreSQL functions with admin/platform-RBAC checks, define one service-role-only uuid overload for internal lookups and one user-context () overload for client usage".

Also applies to: 453-485, 537-542, 543-567

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

In `@supabase/migrations/20260317020451_secure_remaining_helper_rpcs.sql` around
lines 447 - 452, The RPCs get_user_main_org_id(uuid) and
is_account_disabled(uuid) currently accept caller-controlled UUIDs but are
intended to be self-only; add a no-argument client wrapper for each (e.g.,
get_user_main_org_id() and is_account_disabled()) that internally calls
auth.uid() and delegates to the service-role uuid overload, and keep the
existing uuid overload as service-role-only for internal lookups; then adjust
grants so the no-arg overloads are EXECUTE-able by "authenticated" (and not by
PUBLIC/anon) while the uuid overloads are EXECUTE-able only by "service_role"
(revoking PUBLIC/anon as needed).
supabase/tests/47_test_helper_rpc_authz.sql (1)

3-135: Add coverage for the remaining hardened RPCs to fully close the loop.

This suite currently validates a subset of the hardened helpers. Please add explicit authz tests for is_onboarded_org, is_onboarding_needed_org, and is_org_yearly (authorized, foreign-org, and anonymous contexts) so all newly hardened RPCs are regression-protected.

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

In `@supabase/tests/47_test_helper_rpc_authz.sql` around lines 3 - 135, Add
explicit authz tests for is_onboarded_org, is_onboarding_needed_org, and
is_org_yearly by mirroring the existing patterns: for each RPC, authenticate as
test_admin (tests.authenticate_as('test_admin')) and assert the admin can read
the actual boolean/metric (use is(...) or ok(...) similar to
is_canceled_org/get_total_storage_size_org), then authenticate as test_user and
assert a foreign-org user receives the safe non-disclosing value (false or 0 as
appropriate, like the is_canceled_org/get_total_storage_size_org checks), and
finally clear authentication (tests.clear_authentication()) and assert anonymous
calls are non-disclosing—either expect a safe default (false/0) or use
throws_ok(...) with SQLSTATE '42501' if the function should be blocked; use
tests.get_supabase_uid('test_admin') and the UUID
'22dbad8a-b885-4309-9b3b-a09f8460fb6d' to target the org in each case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@supabase/migrations/20260317020451_secure_remaining_helper_rpcs.sql`:
- Around line 447-452: The RPCs get_user_main_org_id(uuid) and
is_account_disabled(uuid) currently accept caller-controlled UUIDs but are
intended to be self-only; add a no-argument client wrapper for each (e.g.,
get_user_main_org_id() and is_account_disabled()) that internally calls
auth.uid() and delegates to the service-role uuid overload, and keep the
existing uuid overload as service-role-only for internal lookups; then adjust
grants so the no-arg overloads are EXECUTE-able by "authenticated" (and not by
PUBLIC/anon) while the uuid overloads are EXECUTE-able only by "service_role"
(revoking PUBLIC/anon as needed).

In `@supabase/tests/47_test_helper_rpc_authz.sql`:
- Around line 3-135: Add explicit authz tests for is_onboarded_org,
is_onboarding_needed_org, and is_org_yearly by mirroring the existing patterns:
for each RPC, authenticate as test_admin (tests.authenticate_as('test_admin'))
and assert the admin can read the actual boolean/metric (use is(...) or ok(...)
similar to is_canceled_org/get_total_storage_size_org), then authenticate as
test_user and assert a foreign-org user receives the safe non-disclosing value
(false or 0 as appropriate, like the is_canceled_org/get_total_storage_size_org
checks), and finally clear authentication (tests.clear_authentication()) and
assert anonymous calls are non-disclosing—either expect a safe default (false/0)
or use throws_ok(...) with SQLSTATE '42501' if the function should be blocked;
use tests.get_supabase_uid('test_admin') and the UUID
'22dbad8a-b885-4309-9b3b-a09f8460fb6d' to target the org in each case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3801b595-e4cc-47a0-b4f0-ef2444e09171

📥 Commits

Reviewing files that changed from the base of the PR and between 877c35b and bde227a.

📒 Files selected for processing (2)
  • supabase/migrations/20260317020451_secure_remaining_helper_rpcs.sql
  • supabase/tests/47_test_helper_rpc_authz.sql

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit fa27cf6 into main Mar 18, 2026
15 checks passed
@riderx riderx deleted the codex/fix-ghsa-hc74-rpc-authz-automation branch March 18, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant