Skip to content

fix(security): patch 9 vulnerabilities across SQL functions and edge endpoints#1735

Closed
ghost wants to merge 7 commits into
mainfrom
unknown repository
Closed

fix(security): patch 9 vulnerabilities across SQL functions and edge endpoints#1735
ghost wants to merge 7 commits into
mainfrom
unknown repository

Conversation

@ghost
Copy link
Copy Markdown

@ghost ghost commented Mar 3, 2026

Summary

This PR addresses 9 security vulnerabilities found during a comprehensive audit, organized in two waves.

Wave 1: Edge Function & RLS Fixes

# Severity File Issue Fix
1 High private/events.ts IDOR — body.user_id overrides auth.uid(), allowing cross-user event injection Removed body override, always use auth.uid()
2 High schemas/prod.sql Manifest table RLS allows cross-org reads via app_id Added org-scoped RLS policy
3 Medium public/organization/index.ts DELETE /members uses middlewareKey(['read']) — read-only keys can delete members Changed to middlewareKey(['all'])
4 Low private/validate_password_compliance.ts Error messages leak whether email exists (credential oracle) Normalized error responses

Wave 2: SECURITY DEFINER Function Grants & RBAC

# Severity Function/File Issue Fix
5 Critical upsert_version_meta() SECURITY DEFINER + GRANT to anon, zero auth — anon can corrupt billing metrics REVOKE from anon + authenticated
6 Critical record_build_time() SECURITY DEFINER + GRANT to anon, zero auth — anon can inject build logs with 2x iOS multiplier REVOKE from anon + authenticated
7 High 7 info-disclosure functions SECURITY DEFINER + GRANT to anon, zero auth — anon can read any org's metrics, plan info, usage stats REVOKE from anon (authenticated kept for frontend)
8 Medium delete_old_deleted_versions() SECURITY DEFINER cron job exposed as public RPC — anon can trigger global data purge REVOKE from anon + authenticated
9 Medium bundle/update_metadata.ts Missing checkPermission() — every other bundle endpoint enforces RBAC except this one Added checkPermission('app.upload_bundle')

Migration Files

  • 20260303000000_fix_manifest_rls_crossorg.sql — Wave 1 RLS fix
  • 20260303000001_revoke_dangerous_security_definer_grants.sql — Wave 2 REVOKE statements

Follow-up Recommendation

The 7 info-disclosure functions (item #7) still allow any authenticated user to query metrics for any org. Adding auth.uid() + org membership checks inside each function body is recommended as a follow-up to fully close this vector.

Related

Test plan

  1. IDOR fix (events.ts): Call POST /private/events with a body.user_id different from the JWT user — verify the event is attributed to the JWT identity, not body.user_id.
  2. Manifest RLS: As user A (org-1), query SELECT * FROM manifest — verify only manifests for org-1 apps are returned, not org-2 manifests.
  3. API key modes (organization endpoints): Use a read-only API key to call DELETE /organization/members — should return 403/unauthorized.
  4. Credential oracle: Call /private/validate_password_compliance with invalid credentials vs valid credentials with wrong org — both should return identical 400 validation_failed response.
  5. REVOKE migration: After running migration, attempt SELECT upsert_version_meta(...) as anon role — should fail with permission denied.
  6. update_metadata RBAC: Call POST /bundle/update_metadata without proper app permissions — should return no_permission error.

Screenshots

N/A — backend-only changes (Edge Functions + SQL migrations). 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.

mailclaude35 added 4 commits March 3, 2026 02:44
…ndpoint

Remove body.user_id override that allowed any authenticated user to inject
events into arbitrary organization Realtime channels. The endpoint now always
uses the authenticated user's ID from JWT (c.get('auth').userId).
Replace the permissive USING(true) SELECT policy on the manifest table with
an org-scoped policy that joins through app_versions to verify the requesting
user belongs to the same organization. Prevents cross-org leak of S3 paths,
file hashes, and sizes.
Restrict PUT and POST routes to ['all','write'] and DELETE routes to ['all']
only. Previously all routes accepted read and upload key modes, allowing
read-only API keys to perform destructive operations (update, create, delete
organizations and members).
Return a generic 400 'validation_failed' error for all failure modes instead
of distinct 401 (invalid credentials) and 403 (not a member) codes. This
prevents unauthenticated attackers from using the endpoint as a credential
oracle to enumerate valid email/password combinations and org membership.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Derives orgId strictly from authenticated identity for events; genericizes password/compliance failures to a 400 validation_failed; tightens organization route API key modes; adds runtime RBAC check for bundle metadata updates; and adds migrations to restrict cross-org manifest reads and revoke dangerous function grants.

Changes

Cohort / File(s) Summary
Auth & Events
supabase/functions/_backend/private/events.ts
Remove fallback to body.user_id; derive orgId only from c.get('auth')?.userId and use it for onboarding-step, org and app lookups.
Password & Login Validation
supabase/functions/_backend/private/validate_password_compliance.ts
Change error responses to generic 400 validation_failed for captcha failures, invalid credentials, and non-membership; add security rationale comments.
Organization API Routes
supabase/functions/_backend/public/organization/index.ts
Reduce allowed key modes for PUT/POST/DELETE and member endpoints (remove read/upload where applicable); add security comment on PUT.
Bundle Upload RBAC
supabase/functions/_backend/public/bundle/update_metadata.ts
Import checkPermission and enforce runtime app.upload_bundle permission check using app_id; return no_permission on denial.
Manifest RLS Migration
supabase/migrations/20260303000000_fix_manifest_rls_crossorg.sql
Drop permissive manifest SELECT policy; add org-scoped SELECT policy using app_versions + check_min_rights and get_identity_org_appid.
Revoke Dangerous Grants Migration
supabase/migrations/20260303000001_revoke_dangerous_security_definer_grants.sql
New migration revoking ALL privileges on multiple sensitive/maintenance functions from anon, authenticated, and/or public roles as appropriate.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Func as HTTP Function
  participant Auth as Auth Context
  participant RBAC as RBAC Service
  participant DB as Database

  Client->>Func: HTTP request (key/token, body with app_id)
  Func->>Auth: read identity (c.get('auth')?.userId)
  Auth-->>Func: auth.userId (orgId)
  Func->>RBAC: checkPermission(app.upload_bundle, app_id, orgId)
  RBAC-->>Func: allowed / denied
  alt allowed
    Func->>DB: query/update app/manifest/app_versions scoped by orgId
    DB-->>Func: rows / success
    Func-->>Client: 200 OK
  else denied
    Func-->>Client: 403 no_permission / 400 validation_failed
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hop through logs with gentle bite,
Tighten the gates, tuck keys out of sight,
Org walls stand firm, migrations sow seeds,
RBAC hums softly as code follows deeds,
I twitch my nose — secure trails indeed.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: security patches for 9 vulnerabilities across SQL functions and edge endpoints, directly matching the scope and objectives of the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections including summary, test plan, and checklist with clear explanations of nine security vulnerabilities across two waves.

✏️ 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

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: 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/functions/_backend/public/organization/index.ts`:
- Line 52: The DELETE /members route is using middlewareKey(['all','write']) but
must require only all-mode keys; update the route registration to call
middlewareKey with only ['all'] for the handler registered on
app.delete('/members', ...), ensuring any related auth checks or tests reference
the stricter mode (middlewareKey(['all']) instead of
middlewareKey(['all','write'])).

In `@supabase/migrations/20260303000000_fix_manifest_rls_crossorg.sql`:
- Around line 15-18: The RLS check is calling get_identity() directly inside the
check_min_rights call; because av.app_id is in scope you must replace the
identity resolver with get_identity_org_appid() so the app-aware identity is
used. Update the call inside the check that invokes "public"."check_min_rights"
(the clause currently passing "public"."get_identity"()) to instead pass
"public"."get_identity_org_appid"(), leaving av.owner_org and av.app_id
parameters in place so check_min_rights receives the app-aware identity.

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

📒 Files selected for processing (4)
  • supabase/functions/_backend/private/events.ts
  • supabase/functions/_backend/private/validate_password_compliance.ts
  • supabase/functions/_backend/public/organization/index.ts
  • supabase/migrations/20260303000000_fix_manifest_rls_crossorg.sql

Comment thread supabase/functions/_backend/public/organization/index.ts Outdated
Comment thread supabase/migrations/20260303000000_fix_manifest_rls_crossorg.sql
mailclaude35 added 2 commits March 3, 2026 02:55
- DELETE /members restricted to ['all'] only (was ['all','write'])
- SQL migration uses get_identity_org_appid() instead of get_identity()
  to follow project coding guidelines for RLS policies with app_id
- Added 'anon' role to manifest RLS policy to match existing patterns
…check to update_metadata

- Revoke anon+authenticated grants on upsert_version_meta, record_build_time,
  delete_old_deleted_versions (backend-only functions exposed to anonymous callers)
- Revoke anon grants on 7 info-disclosure SECURITY DEFINER functions
  (get_app_metrics, get_plan_usage_percent_detailed, get_current_plan_max_org,
  get_current_plan_name_org, get_total_storage_size_org,
  get_total_app_storage_size_orgs, is_good_plan_v5_org)
- Add missing checkPermission() call to bundle/update_metadata endpoint
  (every other bundle endpoint enforces RBAC, this one was skipped)
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: 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/20260303000001_revoke_dangerous_security_definer_grants.sql`:
- Around line 40-66: The listed functions (get_app_metrics overloads,
get_plan_usage_percent_detailed overloads, get_current_plan_max_org,
get_current_plan_name_org, get_total_storage_size_org,
get_total_app_storage_size_orgs, is_good_plan_v5_org) still allow the
authenticated role to bypass org authorization; either remove the remaining
GRANT ALL TO "authenticated" entries in this migration for those functions or
add explicit org-membership checks (e.g., using auth.uid(),
get_identity_org_appid, or equivalent guard logic) inside each function body to
validate the caller’s org before returning data; implement the safer short-term
fix by revoking authenticated rights for the listed functions in this patch, or
alternatively add the guard checks to each function (reference the function
names above when editing).
- Around line 19-66: This migration revokes EXECUTE only from "anon" and
"authenticated" but leaves PUBLIC with permissions; add explicit REVOKE ALL ON
FUNCTION ... FROM PUBLIC for each listed function signature (e.g.,
upsert_version_meta(p_app_id,p_version_id,p_size),
record_build_time(p_org_id,p_user_id,p_build_id,p_platform,p_build_time_unit),
delete_old_deleted_versions(), get_app_metrics(...),
get_plan_usage_percent_detailed(...), get_current_plan_max_org(orgid),
get_current_plan_name_org(orgid), get_total_storage_size_org(org_id),
get_total_app_storage_size_orgs(org_id,app_id), is_good_plan_v5_org(orgid)) so
every targeted function revokes privileges from PUBLIC as done in prior
migrations.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac4bf6f and ba3fadf.

📒 Files selected for processing (2)
  • supabase/functions/_backend/public/bundle/update_metadata.ts
  • supabase/migrations/20260303000001_revoke_dangerous_security_definer_grants.sql

Added REVOKE ALL ... FROM PUBLIC for all targeted SECURITY DEFINER functions.
This closes the privilege gap where PUBLIC pseudo-role could retain EXECUTE.

Note: authenticated role is intentionally kept for 7 info-disclosure functions
because the frontend (Usage.vue, supabase.ts) calls these via authenticated
JWT. Adding auth.uid() + org membership checks inside function bodies is the
proper follow-up to close the cross-tenant vector without breaking the UI.
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 (1)
supabase/migrations/20260303000001_revoke_dangerous_security_definer_grants.sql (1)

39-47: Cross-tenant risk documented; follow-up needed.

The comment correctly identifies that authenticated users can still query any org's data. Since revoking authenticated would break the frontend, the documented follow-up—adding auth.uid() + org membership guards inside each function body—is the appropriate remediation path.

Consider creating a tracking issue for this follow-up to ensure it doesn't slip through.

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

In
`@supabase/migrations/20260303000001_revoke_dangerous_security_definer_grants.sql`
around lines 39 - 47, The SQL migration notes a cross-tenant risk where
authenticated users can access any org's metrics; fix by adding explicit
authorization checks inside each affected function (use auth.uid() and verify
org membership/role) — update every function referenced in this migration to
validate the caller's org membership before returning org-scoped data (e.g., add
membership checks in the bodies of the metrics/plan/usage functions mentioned in
the comment), and open a tracked issue (or link an existing ticket ID) to ensure
the follow-up work to implement these auth guards across all functions is
completed and reviewed.
🤖 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/20260303000001_revoke_dangerous_security_definer_grants.sql`:
- Around line 39-47: The SQL migration notes a cross-tenant risk where
authenticated users can access any org's metrics; fix by adding explicit
authorization checks inside each affected function (use auth.uid() and verify
org membership/role) — update every function referenced in this migration to
validate the caller's org membership before returning org-scoped data (e.g., add
membership checks in the bodies of the metrics/plan/usage functions mentioned in
the comment), and open a tracked issue (or link an existing ticket ID) to ensure
the follow-up work to implement these auth guards across all functions is
completed and reviewed.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba3fadf and d31b413.

📒 Files selected for processing (1)
  • supabase/migrations/20260303000001_revoke_dangerous_security_definer_grants.sql

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 3, 2026

@riderx
Copy link
Copy Markdown
Member

riderx commented Mar 3, 2026

this is not OK, security should be disclosed privately banned

@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.

1 participant