Skip to content

fix(security): add org membership checks to prevent cross-tenant data leakage in SECURITY DEFINER functions#1738

Closed
ghost wants to merge 1 commit into
mainfrom
unknown repository
Closed

fix(security): add org membership checks to prevent cross-tenant data leakage in SECURITY DEFINER functions#1738
ghost wants to merge 1 commit into
mainfrom
unknown repository

Conversation

@ghost
Copy link
Copy Markdown

@ghost ghost commented Mar 3, 2026

Summary

Add auth.uid() + org_users membership verification to 8 SECURITY DEFINER functions that accept an org_id parameter but never check that the caller belongs to the target organization.

Any authenticated user could read metrics, plan info, storage stats, and billing data for ANY organization by passing an arbitrary org_id to these functions.

Affected Functions

Function Return on unauthorized Impact
get_app_metrics(org_id, start_date, end_date) Empty result set MAU, bandwidth, storage, installs per app leaked
get_current_plan_max_org(orgid) Empty result set Plan limits leaked
get_current_plan_name_org(orgid) NULL Stripe plan name leaked
get_plan_usage_percent_detailed(orgid) Empty result set Usage percentages leaked
get_plan_usage_percent_detailed(orgid, cycle_start, cycle_end) Empty result set Custom-range usage percentages leaked
get_total_app_storage_size_orgs(org_id, app_id) 0 Per-app storage size leaked
get_total_storage_size_org(org_id) 0 Total org storage leaked
is_good_plan_v5_org(orgid) FALSE Billing/plan fitness leaked

Implementation

Each function now includes a cross-tenant guard at the top of its body:

IF NOT EXISTS (
    SELECT 1 FROM public.org_users
    WHERE org_users.org_id = <param>
      AND org_users.user_id = (SELECT auth.uid())
) THEN
    RETURN;  -- empty/null/false depending on return type
END IF;

Migration

  • 20260303100000_add_org_membership_checks.sqlCREATE OR REPLACE FUNCTION for all 8 functions

Closes #1737

Test plan

  1. As User A (member of Org-1), call SELECT * FROM get_plan_usage_percent_detailed('org-1-uuid') — should return usage data normally.
  2. As User B (member of Org-2), call SELECT * FROM get_plan_usage_percent_detailed('org-1-uuid') — should return empty result set.
  3. Repeat for all 8 functions with cross-org org_id — all should return empty/null/0/false.
  4. Verify frontend Usage.vue and supabase.ts still work correctly (they always pass the user's own org_id).
  5. Verify get_app_metrics(org_id) single-arg wrapper still works (delegates to the 3-arg version which has the check).

Screenshots

N/A — backend-only changes (SQL migration). No UI changes.

Checklist

  • My code follows the code style of this project and passes bun run lint:backend && bun run lint.
  • My change requires a change to the documentation.
  • I have tested my code manually, and I have provided steps how to reproduce my tests.
  • My change has adequate test coverage via the test plan above.

Summary by CodeRabbit

  • New Features
    • Enhanced organization data access controls with built-in membership verification for metrics, plan usage, and storage tracking.

…closure functions

Add auth.uid() + org_users membership verification to 8 SECURITY DEFINER
functions that accept org_id but never verify the caller belongs to that
organization. Any authenticated user could query metrics, plan info, storage
stats, and billing data for ANY organization by passing an arbitrary org_id.

Affected functions:
- get_app_metrics(org_id, start_date, end_date)
- get_current_plan_max_org(orgid)
- get_current_plan_name_org(orgid)
- get_plan_usage_percent_detailed(orgid) [both overloads]
- get_total_app_storage_size_orgs(org_id, app_id)
- get_total_storage_size_org(org_id)
- is_good_plan_v5_org(orgid)

Each function now returns empty/null/false if the caller is not a member
of the target organization, preventing cross-tenant data leakage.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

This pull request adds org membership verification guards to eight existing PostgreSQL SECURITY DEFINER functions, ensuring that authenticated users can only access metrics, plan details, and storage data for organizations they belong to, preventing cross-tenant information disclosure.

Changes

Cohort / File(s) Summary
Org-Scoped Metrics & Plan Functions
supabase/migrations/20260303100000_add_org_membership_checks.sql
Eight new SECURITY DEFINER functions added: get_app_metrics, get_current_plan_max_org, get_current_plan_name_org, get_plan_usage_percent_detailed (2 overloads), get_total_app_storage_size_orgs, get_total_storage_size_org, and is_good_plan_v5_org. Each function verifies org membership via public.org_users before querying sensitive billing, metrics, and storage data. Functions return empty results if user lacks authorization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Eight guards we place, oh so keen,
No peeking at neighbors' billing scene!
Org membership checks, standing tall,
Protecting each tenant's data from all.
Security strengthened, warren stays free! 🔐

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding org membership checks to SECURITY DEFINER functions to prevent cross-tenant data leakage.
Description check ✅ Passed The description includes a comprehensive summary, detailed impact analysis, implementation details, migration file reference, and a thorough test plan with specific reproduction steps.
Linked Issues check ✅ Passed The PR fully addresses the security vulnerability documented in issue #1737 by adding org_users membership checks to all eight affected SECURITY DEFINER functions.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the cross-tenant information disclosure vulnerability in the eight SECURITY DEFINER functions; no unrelated modifications are present.
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
  • Post copyable unit tests in a comment

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/20260303100000_add_org_membership_checks.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


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.

❤️ Share

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

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

🤖 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/20260303100000_add_org_membership_checks.sql`:
- Around line 3-17: Update the migration comment header so the function count
matches the list: change "These 7 info-disclosure functions" to "These 8
info-disclosure functions" (or remove the numeric count entirely) so it
correctly reflects the eight listed functions (get_app_metrics,
get_current_plan_max_org, get_current_plan_name_org,
get_plan_usage_percent_detailed (both overloads count as one or two as
appropriate), get_total_app_storage_size_orgs, get_total_storage_size_org,
is_good_plan_v5_org).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between effb8b6 and 2020d8a.

📒 Files selected for processing (1)
  • supabase/migrations/20260303100000_add_org_membership_checks.sql

Comment on lines +3 to +17
-- These 7 info-disclosure functions accept an org_id parameter but never verify
-- that the calling user belongs to that organization. Any authenticated user can
-- query metrics, plan info, and usage stats for ANY organization by passing an
-- arbitrary org_id. This migration adds org_users membership checks so only
-- members of an organization can access its data.
--
-- Affected functions:
-- 1. get_app_metrics(org_id, start_date, end_date)
-- 2. get_current_plan_max_org(orgid)
-- 3. get_current_plan_name_org(orgid)
-- 4. get_plan_usage_percent_detailed(orgid)
-- 5. get_plan_usage_percent_detailed(orgid, cycle_start, cycle_end)
-- 6. get_total_app_storage_size_orgs(org_id, app_id)
-- 7. get_total_storage_size_org(org_id)
-- 8. is_good_plan_v5_org(orgid)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the header count mismatch in the migration comment.

Line 3 says “7 info-disclosure functions,” but Lines 10-17 list 8.

✏️ Proposed fix
--- a/supabase/migrations/20260303100000_add_org_membership_checks.sql
+++ b/supabase/migrations/20260303100000_add_org_membership_checks.sql
@@
--- These 7 info-disclosure functions accept an org_id parameter but never verify
+-- These 8 info-disclosure functions accept an org_id parameter but never verify
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260303100000_add_org_membership_checks.sql` around
lines 3 - 17, Update the migration comment header so the function count matches
the list: change "These 7 info-disclosure functions" to "These 8 info-disclosure
functions" (or remove the numeric count entirely) so it correctly reflects the
eight listed functions (get_app_metrics, get_current_plan_max_org,
get_current_plan_name_org, get_plan_usage_percent_detailed (both overloads count
as one or two as appropriate), get_total_app_storage_size_orgs,
get_total_storage_size_org, is_good_plan_v5_org).

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 3, 2026

@riderx
Copy link
Copy Markdown
Member

riderx commented Mar 4, 2026

AI SPAM

@riderx riderx closed this Mar 4, 2026
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.

fix(security): Cross-tenant information disclosure via SECURITY DEFINER functions missing org membership checks

1 participant