Files
strix/strix/skills/technologies/supabase.jinja

190 lines
12 KiB
Django/Jinja
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<supabase_security_guide>
<title>SUPABASE — ADVERSARIAL TESTING AND EXPLOITATION</title>
<critical>Supabase exposes Postgres through PostgREST, Realtime, GraphQL, Storage, Auth (GoTrue), and Edge Functions. Most impactful findings come from mis-scoped Row Level Security (RLS), unsafe RPCs, leaked service_role keys, lax Storage policies, GraphQL overfetching, and Edge Functions trusting headers or tokens without binding to issuer/audience/tenant.</critical>
<scope>
- PostgREST: table CRUD, filters, embeddings, RPC (remote functions)
- RLS: row ownership/tenant isolation via policies and auth.uid()
- Storage: buckets, objects, signed URLs, public/private policies
- Realtime: replication subscriptions, broadcast/presence channels
- GraphQL: pg_graphql over Postgres schema with RLS interaction
- Auth (GoTrue): JWTs, cookie/session, magic links, OAuth flows
- Edge Functions (Deno): server-side code calling Supabase with secrets
</scope>
<methodology>
1. Inventory surfaces: REST /rest/v1, Storage /storage/v1, GraphQL /graphql/v1, Realtime wss, Auth /auth/v1, Functions https://<project>.functions.supabase.co/.
2. Obtain tokens for: unauth (anon), basic user, other user, and (if disclosed) admin/staff; enumerate anon key exposure and verify if service_role leaked anywhere.
3. Build a Resource × Action × Principal matrix and test each via REST and GraphQL. Confirm parity across channels and content-types (json/form/multipart).
4. Start with list/search/export endpoints to gather IDs, then attempt direct reads/writes across principals, tenants, and transports. Validate RLS and function guards.
</methodology>
<architecture>
- Project endpoints: https://<ref>.supabase.co; REST at /rest/v1/<table>, RPC at /rest/v1/rpc/<fn>.
- Headers: apikey: <anon-or-service>, Authorization: Bearer <JWT>. Anon key only identifies the project; JWT binds user context.
- Roles: anon, authenticated; service_role bypasses RLS and must never be client-exposed.
- auth.uid(): current user UUID claim; policies must never trust client-supplied IDs over server context.
</architecture>
<rls>
- Enable RLS on every non-public table; absence or “permit-all” policies → bulk exposure.
- Common gaps:
- Policies check auth.uid() for read but forget UPDATE/DELETE/INSERT.
- Missing tenant constraints (org_id/tenant_id) allow cross-tenant reads/writes.
- Policies rely on client-provided columns (user_id in payload) instead of deriving from JWT.
- Complex joins where the effective policy is applied after filters, enabling inference via counts or projections.
- Tests:
- Compare results for two users: GET /rest/v1/<table>?select=*&Prefer=count=exact; diff row counts and IDs.
- Try cross-tenant: add &org_id=eq.<other_org> or use or=(org_id.eq.other,org_id.is.null).
- Write-path: PATCH/DELETE single row with foreign id; INSERT with foreign owner_id then read.
</rls>
<postgrest_and_rest>
- Filters: eq, neq, lt, gt, ilike, or, is, in; embed relations with select=*,profile(*); exploit embeddings to overfetch linked rows if resolvers skip per-row checks.
- Headers to know: Prefer: return=representation (echo writes), Prefer: count=exact (exposure via counts), Accept-Profile/Content-Profile to select schema.
- IDOR patterns: /rest/v1/<table>?select=*&id=eq.<other_id>; query alternative keys (slug, email) and composite keys.
- Search leaks: generous LIKE/ILIKE filters + lack of RLS → mass disclosure.
- Mass assignment: if RPC not used, PATCH can update unintended columns; verify restricted columns via database permissions/policies.
</postgrest_and_rest>
<rpc_functions>
- RPC endpoints map to SQL functions. SECURITY DEFINER bypasses RLS unless carefully coded; SECURITY INVOKER respects caller.
- Anti-patterns:
- SECURITY DEFINER + missing owner checks → vertical/horizontal bypass.
- set search_path left to public; function resolves unsafe objects.
- Trusting client-supplied user_id/tenant_id rather than auth.uid().
- Tests:
- Call /rest/v1/rpc/<fn> as different users with foreign ids in body.
- Remove or alter JWT entirely (Authorization: Bearer <anon>) to see if function still executes.
- Validate that functions perform explicit ownership/tenant checks inside SQL, not only in docs.
</rpc_functions>
<storage>
- Buckets: public vs private; objects live in storage.objects with RLS-like policies.
- Find misconfigs:
- Public buckets holding sensitive data: GET https://<ref>.supabase.co/storage/v1/object/public/<bucket>/<path>
- Signed URLs with long TTL and no audience binding; reuse/guess tokens across tenants/paths.
- Listing prefixes without auth: /storage/v1/object/list/<bucket>?prefix=
- Path confusion: mixed case, URL-encoding, “..” segments rejected at UI but accepted by API.
- Abuse vectors:
- Content-type/XSS: upload HTML/SVG served as text/html or image/svg+xml; confirm X-Content-Type-Options: nosniff and Content-Disposition: attachment.
- Signed URL replay across accounts/buckets if validation is lax.
</storage>
<realtime>
- Endpoint: wss://<ref>.supabase.co/realtime/v1. Join channels with apikey + Authorization.
- Risks:
- Channel names derived from table/schema/filters leaking other users updates when RLS or channel guards are weak.
- Broadcast/presence channels allowing cross-room join/publish without auth checks.
- Tests:
- Subscribe to public:realtime changes on protected tables; confirm row data visibility aligns with RLS.
- Attempt joining other users presence/broadcast channels (e.g., room:<user_id>, org:<id>).
</realtime>
<graphql>
- Endpoint: /graphql/v1 using pg_graphql with RLS. Risks:
- Introspection reveals schema relations; ensure its intentional.
- Overfetch via nested relations where field resolvers fail to re-check ownership/tenant.
- Global node IDs (if implemented) leaked and reusable via different viewers.
- Tests:
- Compare REST vs GraphQL responses for the same principal and query shape.
- Query deep nested fields and connections; verify RLS holds at each edge.
</graphql>
<auth_and_tokens>
- GoTrue issues JWTs with claims (sub=uid, role, aud=authenticated). Validate on server: issuer, audience, exp, signature, and tenant context.
- Pitfalls:
- Storing tokens in localStorage → XSS exfiltration; refresh mismanagement leading to long-lived sessions.
- Treating apikey as identity; it is project-scoped, not user identity.
- Exposing service_role key in client bundle or Edge Function responses.
- Tests:
- Replay tokens across services; check audience/issuer pinning.
- Try downgraded tokens (expired/other audience) against custom endpoints.
</auth_and_tokens>
<edge_functions>
- Deno-based functions often initialize server-side Supabase client with service_role. Risks:
- Trusting Authorization/apikey headers without verifying JWT against issuer/audience.
- CORS: wildcard origins with credentials; reflected Authorization in responses.
- SSRF via fetch; secrets exposed via error traces or logs.
- Tests:
- Call functions with and without Authorization; compare behavior.
- Try foreign resource IDs in function payloads; verify server re-derives user/tenant from JWT.
- Attempt to reach internal endpoints (metadata services, project endpoints) via function fetch.
</edge_functions>
<tenant_isolation>
- Ensure every query joins or filters by tenant_id/org_id derived from JWT context, not client input.
- Tests:
- Change subdomain/header/path tenant selectors while keeping JWT tenant constant; look for cross-tenant data.
- Export/report endpoints: confirm queries execute under caller scope; signed outputs must encode tenant and short TTL.
</tenant_isolation>
<bypass_techniques>
- Content-type switching: application/json ↔ application/x-www-form-urlencoded ↔ multipart/form-data to hit different code paths.
- Parameter pollution: duplicate keys in JSON/query; PostgREST chooses last/first depending on parser.
- GraphQL+REST parity probing: protections often drift; fetch via the weaker path.
- Race windows: parallel writes to bypass post-insert ownership updates.
</bypass_techniques>
<blind_channels>
- Use Prefer: count=exact and ETag/length diffs to infer unauthorized rows.
- Conditional requests (If-None-Match) to detect object existence without content exposure.
- Storage signed URLs: timing/length deltas to map valid vs invalid tokens.
</blind_channels>
<tooling_and_automation>
- PostgREST: httpie/curl + jq; enumerate tables with known names; fuzz filters (or=, ilike, neq, is.null).
- GraphQL: graphql-inspector, voyager; build deep queries to test field-level enforcement; complexity/batching tests.
- Realtime: custom ws client; subscribe to suspicious channels/tables; diff payloads per principal.
- Storage: enumerate bucket listing APIs; script signed URL generation/use patterns.
- Auth/JWT: jwt-cli/jose to validate audience/issuer; replay against Edge Functions.
- Policy diffing: maintain request sets per role and compare results across releases.
</tooling_and_automation>
<reviewer_checklist>
- Are all non-public tables RLS-enabled with explicit SELECT/INSERT/UPDATE/DELETE policies?
- Do policies derive subject/tenant from JWT (auth.uid(), tenant claim) rather than client payload?
- Do RPC functions run as SECURITY INVOKER, or if DEFINER, do they enforce ownership/tenant inside?
- Are Storage buckets private by default, with short-lived signed URLs bound to tenant/context?
- Does Realtime enforce RLS-equivalent filtering for subscriptions and block cross-room joins?
- Is GraphQL parity verified with REST; are nested resolvers guarded per field?
- Are Edge Functions verifying JWT (issuer/audience) and never exposing service_role to clients?
- Are CDN/cache keys bound to Authorization/tenant to prevent cache leaks?
</reviewer_checklist>
<validation>
1. Provide owner vs non-owner requests for REST/GraphQL showing unauthorized access (content or metadata).
2. Demonstrate a mis-scoped RPC or Storage signed URL usable by another user/tenant.
3. Confirm Realtime or GraphQL exposure matches missing policy checks.
4. Document minimal reproducible requests and role contexts used.
</validation>
<false_positives>
- Tables intentionally public (documented) with non-sensitive content.
- RLS-enabled tables returning only caller-owned rows; mismatched UI not backed by API responses.
- Signed URLs with very short TTL and audience binding.
- Edge Functions verifying tokens and re-deriving context before acting.
</false_positives>
<impact>
- Cross-account/tenant data exposure and unauthorized state changes.
- Exfiltration of PII/PHI/PCI, financial and billing artifacts, private files.
- Privilege escalation via RPC and Edge Functions; durable access via long-lived tokens.
- Regulatory and contractual violations stemming from tenant isolation failures.
</impact>
<pro_tips>
1. Start with /rest/v1 list/search; counts and embeddings reveal policy drift fast.
2. Treat UUIDs and signed URLs as untrusted; validate binding to subject/tenant and TTL.
3. Focus on RPC and Edge Functions—they often centralize business logic and skip RLS.
4. Test GraphQL and Realtime parity with REST; differences are where vulnerabilities hide.
5. Keep role-separated request corpora and diff responses across deployments.
6. Never assume apikey == identity; only JWT binds subject. Prove it.
7. Prefer concise PoCs: one request per role that clearly shows the unauthorized delta.
</pro_tips>
<remember>RLS must bind subject and tenant on every path, and server-side code (RPC/Edge) must re-derive identity from a verified token. Any gap in binding, audience/issuer verification, or per-field enforcement becomes a cross-account or cross-tenant vulnerability.</remember>
</supabase_security_guide>