Skip to content

[codex] fix scoped API key org boundary bypass#1951

Merged
riderx merged 4 commits intomainfrom
codex/fix-ghsa-ccm4-hf72-p28m
Apr 27, 2026
Merged

[codex] fix scoped API key org boundary bypass#1951
riderx merged 4 commits intomainfrom
codex/fix-ghsa-ccm4-hf72-p28m

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 24, 2026

Summary (AI generated)

  • enforce API key org and app scope inside rbac_check_permission_direct before any owner-user fallback can authorize out-of-scope access
  • stop DELETE /organization from touching storage until the org row delete actually succeeds
  • only remove member role bindings after the membership delete succeeds, and add RBAC plus API regressions for the advisory paths

Motivation (AI generated)

Scoped API keys were able to inherit the owning user's broader permissions and cross organization boundaries on destructive organization-management routes.

Business Impact (AI generated)

This closes a high-severity tenant-isolation bug that could let delegated integrations damage another organization's branding or RBAC state, which directly affects customer trust and incident risk.

Test Plan (AI generated)

  • bun run lint:backend
  • bunx eslint tests/rbac-permissions.test.ts tests/organization-api.test.ts
  • bun typecheck
  • bun run supabase:with-env -- bunx vitest run tests/rbac-permissions.test.ts tests/organization-api.test.ts

Generated with AI

Summary by CodeRabbit

  • Bug Fixes

    • Improved error handling for invalid organization deletions and missing organization members (returns proper 403/404 responses).
  • Security

    • Added stricter permission checks that enforce API-key org/app scoping, 2FA and password-policy rules to prevent cross-organization actions.
  • Tests

    • Added end-to-end tests validating scoped API key behavior and RBAC permission checks; updated tests to use dynamic auth headers.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Enforces API-key org/app scoping in RBAC checks via two new DB functions, simplifies organization deletion by removing storage cleanup and only deleting the org record, tightens member deletion to verify the removed row, and adds tests asserting scoped API keys cannot act across org boundaries.

Changes

Cohort / File(s) Summary
Organization deletion & member removal
supabase/functions/_backend/public/organization/delete.ts, supabase/functions/_backend/public/organization/members/delete.ts
Org delete now removes only the org record using supabaseApikey and verifies deletion with .delete().select('id').maybeSingle(); all Supabase Storage image cleanup and related error paths removed. Member delete now returns the deleted row and throws 404 organization_member_not_found if none deleted; RBAC role-binding cleanup still executed.
RBAC permission-check SQL
supabase/migrations/20260424094101_enforce_apikey_scope_in_rbac_check.sql
Adds public.rbac_check_permission_direct and _no_password_policy functions that validate API keys (including scope to orgs/apps), enforce 2FA, evaluate RBAC (with channel overrides) or fall back to legacy checks, log denials, and update ownership/execute grants.
Tests: scoped API-key behavior & auth headers
tests/organization-api.test.ts, tests/rbac-permissions.test.ts, tests/apikeys-expiration.test.ts
New E2E tests create limited-scope API keys and assert cross-org destructive operations are denied and leave no cross-org side effects; RBAC tests exercise both legacy and new RBAC modes with scoped keys. tests/apikeys-expiration.test.ts switches to dynamic authHeaders setup for requests.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant TS as "TypeScript Handler"
    participant DB as "Database"
    participant RBAC as "public.rbac_check_permission_direct"
    participant Key as "public.find_apikey_by_value"
    participant Scope as "Org/App Scope Check"
    participant TwoFA as "2FA Enforcement"
    participant Perm as "RBAC / Legacy Permission Check"

    Client->>TS: DELETE /organization?orgId=...
    TS->>DB: call rbac_check_permission_direct(..., p_apikey)
    DB->>RBAC: evaluate permission
    RBAC->>Key: validate apikey
    alt apikey missing/expired
        Key-->>RBAC: not found / expired
        RBAC->>DB: log denial
        RBAC-->>TS: false
    else apikey valid
        Key-->>RBAC: apikey data (limited_to_orgs/apps, user_id)
        RBAC->>Scope: check limited_to_orgs/apps
        alt outside scope
            Scope-->>RBAC: denied
            RBAC-->>TS: false
        else within scope
            Scope-->>RBAC: allowed
            RBAC->>TwoFA: check org requires 2FA / user 2FA
            alt 2FA required and user lacks it
                TwoFA-->>RBAC: denied
                RBAC->>DB: log denial
                RBAC-->>TS: false
            else 2FA ok or not required
                TwoFA-->>RBAC: ok
                RBAC->>Perm: evaluate RBAC (and channel overrides) or legacy fallback
                Perm-->>RBAC: permission result
                RBAC-->>TS: true/false
            end
        end
    end
    alt permitted
        TS->>DB: delete org record
        DB-->>TS: success
        TS-->>Client: 200 / success
    else denied
        TS-->>Client: 403 invalid_org_id
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped through keys and orgly trails,
I checked each scope and guarded rails.
No cross‑border nibble now,
RBAC, 2FA—a careful bow,
Carrots encrypted in secure details. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[codex] fix scoped API key org boundary bypass' directly describes the primary security fix addressing the scoped API key organization boundary bypass vulnerability.
Description check ✅ Passed The PR description includes all major template sections: Summary, Motivation/Business Impact, and Test Plan with execution steps; all items are marked as completed.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-ghsa-ccm4-hf72-p28m

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 24, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/fix-ghsa-ccm4-hf72-p28m (a56e6a3) with main (d2ddad3)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review April 24, 2026 11:20
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@riderx
Copy link
Copy Markdown
Member Author

riderx commented Apr 24, 2026

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 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 (2)
supabase/migrations/20260424094101_enforce_apikey_scope_in_rbac_check.sql (1)

1-3: Consider adding explicit privilege controls for these SECURITY DEFINER functions.

Per coding guidelines, non-trigger PostgreSQL RPC functions should apply minimum privileges: REVOKE ALL FROM PUBLIC and grant only to required roles. These callable SECURITY DEFINER functions run with elevated privileges and should restrict who can invoke them.

♻️ Suggested privilege controls (append after each function)
-- After rbac_check_permission_direct (line 250)
ALTER FUNCTION "public"."rbac_check_permission_direct"("text", "uuid", "uuid", character varying, bigint, "text") OWNER TO "postgres";
REVOKE ALL ON FUNCTION "public"."rbac_check_permission_direct"("text", "uuid", "uuid", character varying, bigint, "text") FROM PUBLIC;
GRANT EXECUTE ON FUNCTION "public"."rbac_check_permission_direct"("text", "uuid", "uuid", character varying, bigint, "text") TO "service_role";
GRANT EXECUTE ON FUNCTION "public"."rbac_check_permission_direct"("text", "uuid", "uuid", character varying, bigint, "text") TO "authenticated";

-- After rbac_check_permission_direct_no_password_policy (line 426)
ALTER FUNCTION "public"."rbac_check_permission_direct_no_password_policy"("text", "uuid", "uuid", character varying, bigint, "text") OWNER TO "postgres";
REVOKE ALL ON FUNCTION "public"."rbac_check_permission_direct_no_password_policy"("text", "uuid", "uuid", character varying, bigint, "text") FROM PUBLIC;
GRANT EXECUTE ON FUNCTION "public"."rbac_check_permission_direct_no_password_policy"("text", "uuid", "uuid", character varying, bigint, "text") TO "service_role";
GRANT EXECUTE ON FUNCTION "public"."rbac_check_permission_direct_no_password_policy"("text", "uuid", "uuid", character varying, bigint, "text") TO "authenticated";

As per coding guidelines: "For PostgreSQL RPC and helper functions, apply minimum privileges explicitly: start from deny-by-default, set OWNER explicitly, use REVOKE ALL ... FROM PUBLIC, and grant only required roles."

Also applies to: 253-255

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

In `@supabase/migrations/20260424094101_enforce_apikey_scope_in_rbac_check.sql`
around lines 1 - 3, Add explicit privilege controls for the SECURITY DEFINER
functions: for "rbac_check_permission_direct" and
"rbac_check_permission_direct_no_password_policy" set the OWNER to postgres,
revoke all privileges from PUBLIC, and grant EXECUTE only to the required roles
(service_role and authenticated); append these ALTER/REVOKE/GRANT statements
right after each function definition so the functions are deny-by-default and
only callable by the intended roles.
supabase/functions/_backend/public/organization/members/delete.ts (1)

58-67: Note: Explicit role_bindings delete may be redundant with database trigger.

Per supabase/migrations/20260311162400_sync_org_user_delete_role_bindings.sql, the sync_org_user_role_binding_on_delete trigger already calls resync_org_user_role_bindings to cascade-delete role_bindings when an org_users record is deleted.

This explicit delete serves as a safety net, which is fine. Just noting for awareness that both mechanisms will attempt the cleanup.

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

In `@supabase/functions/_backend/public/organization/members/delete.ts` around
lines 58 - 67, The explicit delete of role_bindings in the block using
supabaseAdmin(...).from('role_bindings').delete() is potentially redundant with
the DB trigger sync_org_user_role_binding_on_delete defined in the migration, so
either remove this explicit delete to rely on the trigger, or keep it but add a
clear comment above the delete referencing the trigger/migration
(sync_org_user_role_binding_on_delete in
20260311162400_sync_org_user_delete_role_bindings.sql) and that it is a
safety-net; if you keep it, leave the existing error handling (throw
simpleError('error_deleting_role_bindings', ...)) as-is.
🤖 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/rbac-permissions.test.ts`:
- Around line 256-297: The test fails to grant any RBAC role to the scoped API
key for allowedOrgId, so rbac_check_permission_direct finds no role_bindings;
insert a role_bindings entry for principal_type='apikey' with principal_id equal
to the apikey's rbac_id (lookup by scopedKey) and org_id = allowedOrgId,
pointing at a role that contains the 'org.delete' permission (or create a role +
role_permissions mapping that includes 'org.delete') before calling
rbac_check_permission_direct in the allowedResult block so the API key check can
succeed (references: rbac_check_permission_direct, scopedKey, allowedOrgId,
USER_ID, role_bindings, apikeys, roles/role_permissions).

---

Nitpick comments:
In `@supabase/functions/_backend/public/organization/members/delete.ts`:
- Around line 58-67: The explicit delete of role_bindings in the block using
supabaseAdmin(...).from('role_bindings').delete() is potentially redundant with
the DB trigger sync_org_user_role_binding_on_delete defined in the migration, so
either remove this explicit delete to rely on the trigger, or keep it but add a
clear comment above the delete referencing the trigger/migration
(sync_org_user_role_binding_on_delete in
20260311162400_sync_org_user_delete_role_bindings.sql) and that it is a
safety-net; if you keep it, leave the existing error handling (throw
simpleError('error_deleting_role_bindings', ...)) as-is.

In `@supabase/migrations/20260424094101_enforce_apikey_scope_in_rbac_check.sql`:
- Around line 1-3: Add explicit privilege controls for the SECURITY DEFINER
functions: for "rbac_check_permission_direct" and
"rbac_check_permission_direct_no_password_policy" set the OWNER to postgres,
revoke all privileges from PUBLIC, and grant EXECUTE only to the required roles
(service_role and authenticated); append these ALTER/REVOKE/GRANT statements
right after each function definition so the functions are deny-by-default and
only callable by the intended roles.
🪄 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: 56aebb43-f5fe-41a6-be42-b4f0a041f918

📥 Commits

Reviewing files that changed from the base of the PR and between 633aeea and 78145c9.

📒 Files selected for processing (5)
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/public/organization/members/delete.ts
  • supabase/migrations/20260424094101_enforce_apikey_scope_in_rbac_check.sql
  • tests/organization-api.test.ts
  • tests/rbac-permissions.test.ts

Comment thread tests/rbac-permissions.test.ts
@sonarqubecloud
Copy link
Copy Markdown

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)
tests/apikeys-expiration.test.ts (1)

14-16: Optional: default authHeaders in apiFetch to remove repetition.

This can reduce boilerplate and prevent future missed headers in new test cases.

♻️ Proposed refactor
 function apiFetch(path: string, init?: RequestInit) {
-  return fetchWithRetry(`${BASE_URL}${path}`, init)
+  return fetchWithRetry(`${BASE_URL}${path}`, {
+    ...init,
+    headers: init?.headers ?? authHeaders,
+  })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/apikeys-expiration.test.ts` around lines 14 - 16, The apiFetch helper
currently calls fetchWithRetry without default headers; update the
apiFetch(path: string, init?: RequestInit) function to merge a default
authHeaders (e.g., Authorization: `Bearer ${TEST_API_KEY}` or reuse existing
authHeaders constant) into init.headers so tests don’t need to repeat headers;
ensure merging preserves any headers passed in by callers and still forwards
init to fetchWithRetry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/apikeys-expiration.test.ts`:
- Around line 14-16: The apiFetch helper currently calls fetchWithRetry without
default headers; update the apiFetch(path: string, init?: RequestInit) function
to merge a default authHeaders (e.g., Authorization: `Bearer ${TEST_API_KEY}` or
reuse existing authHeaders constant) into init.headers so tests don’t need to
repeat headers; ensure merging preserves any headers passed in by callers and
still forwards init to fetchWithRetry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4dbbd539-d81a-497a-8ad3-d902e0b45745

📥 Commits

Reviewing files that changed from the base of the PR and between fc01bab and a56e6a3.

📒 Files selected for processing (1)
  • tests/apikeys-expiration.test.ts

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