Skip to content
GitHub
Security Issues

OAuth auto-provisioning defaults to admin role — privilege escalation

OAuth Auto-Provisioning Defaults to Admin Role

Severity: P0 (privilege escalation) · Applies to: Supabase Auth + custom OAuth provisioning callback Related: ADR-039 — Supabase Auth confirmation and hardening · Authentication runbook

Problem

First-time OAuth users (Google, GitHub) were automatically assigned account_role="admin" during provisioning. This gave every new external user full account-level privileges immediately upon first login.

Symptoms:

  • Any user authenticating via OAuth gets admin access without invitation
  • New users can create unlimited workspaces, invite/remove users
  • No separation between “first user” (should be admin) and “subsequent users”

Root Cause

In provision_oauth_user(), the role defaulting logic was:

role = account_role or "admin" # Every new user becomes admin

This was a shortcut during initial development — the intent was “first user should be admin” but the implementation made ALL users admin.

Solution

Changed the default to "member" (least-privilege):

# Before (dangerous)
role = account_role or "admin"
# After (least-privilege)
role = account_role or "member"

Location: packages/core/src/spectral/infrastructure/auth/supabase_auth.py:294

Implementation Notes

  • The member role has no account-level privileges — only workspace membership
  • Workspace access requires explicit invitation from a workspace owner
  • Admin provisioning should be done through a separate administrative flow
  • The first user of a new account can be promoted to admin via Supabase Studio or direct database access

Prevention

Best Practices

  • Default to least-privilege — new users should have minimal access
  • Separate “create account” from “grant admin” — these are different operations
  • Explicit role assignment — roles should never be implied by authentication method
  • Test the default — unit tests should verify the default role is not elevated

Warning Signs

  • Any code path where role defaults to "admin" or "owner" without explicit check
  • User provisioning functions that don’t require an existing admin to authorize
  • OAuth callbacks that grant more access than email/password registration
def test_provisions_new_user_as_member(self):
"""New OAuth users get member role, not admin."""
role = provision_oauth_user(user_id)
assert role == "member" # Least-privilege default

References

  • Fixed in session 2026-03-21 (review findings)
  • OWASP: Broken Access Control (A01:2021)