Skip to content

Instantly share code, notes, and snippets.

@iagocavalcante
Created March 25, 2026 19:30
Show Gist options
  • Select an option

  • Save iagocavalcante/8b2ca5283cbf34d32e7124bfe0951ea1 to your computer and use it in GitHub Desktop.

Select an option

Save iagocavalcante/8b2ca5283cbf34d32e7124bfe0951ea1 to your computer and use it in GitHub Desktop.
Supabase Security Audit - RLS & Access Control Analysis

Supabase Security Audit Report

Target: zairmalpublcagnbodsi.supabase.co Date: 2026-03-25 Scope: External reconnaissance & configuration analysis Endpoint tested: /rest/v1/formas_recebimento?select=codigo,nome&company_id=eq.<uuid>


Executive Summary

This audit identifies multiple security risks in a Supabase-backed application exposing a PostgREST API. The primary concerns are around tenant isolation (IDOR), overly permissive CORS, schema leakage, and potential Row-Level Security (RLS) misconfigurations. Without access to the Supabase dashboard or the anon key, findings are based on external observation and known Supabase architecture patterns.


Findings

CRITICAL — Potential IDOR via Client-Side Tenant Filtering

Field Value
Severity Critical
CVSS 8.6 (High)
Category OWASP A01:2021 — Broken Access Control

Description: The endpoint filters data by company_id as a query parameter:

/rest/v1/formas_recebimento?select=codigo,nome&company_id=eq.e11cdf51-b57a-4fc2-b415-c864dcdaeb26

This is a client-side filter, not a server-side access control. Any authenticated user (or anyone with the anon key) can replace the UUID with another company's ID to access their payment methods.

Impact: Full cross-tenant data access. An attacker can enumerate all companies' formas_recebimento records.

Remediation:

-- Enable RLS on the table
ALTER TABLE formas_recebimento ENABLE ROW LEVEL SECURITY;

-- Create a policy that restricts access to the user's own company
CREATE POLICY "Users can only view their company's data"
  ON formas_recebimento
  FOR SELECT
  USING (company_id = (auth.jwt() ->> 'company_id')::uuid);

HIGH — Overly Permissive CORS Configuration

Field Value
Severity High
Category OWASP A05:2021 — Security Misconfiguration

Observed headers:

access-control-allow-origin: *
access-control-allow-methods: GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS,TRACE,CONNECT
access-control-max-age: 3600

Issues:

  1. Access-Control-Allow-Origin: * — Any website can make requests to this API. A malicious site could make authenticated requests on behalf of a logged-in user (if cookies/tokens are involved).
  2. All HTTP methods allowed — Including TRACE and CONNECT, which are rarely needed and can be used for cross-site tracing (XST) attacks.
  3. PUT, PATCH, POST, DELETE allowed from any origin — If the anon key is embedded in a frontend, any malicious site can perform write/delete operations.

Remediation:

  • Restrict Access-Control-Allow-Origin to your specific frontend domain(s).
  • Remove TRACE and CONNECT from allowed methods.
  • Use Supabase custom CORS configuration or an API gateway.

Note: This is Supabase's default CORS configuration. While Supabase relies on API keys + RLS rather than CORS for security, the permissive CORS combined with a client-embedded anon key means any website can call your API with valid credentials.


HIGH — Schema and Data Model Leakage

Field Value
Severity High
Category OWASP A04:2021 — Insecure Design

Exposed information from the URL alone:

Leaked Item Value Risk
Table name formas_recebimento Reveals DB schema — attacker knows table structure
Column names codigo, nome Reveals data model — aids further exploitation
Tenant ID e11cdf51-b57a-4fc2-b415-c864dcdaeb26 Real UUID — enables IDOR enumeration
Language/locale Portuguese table/column names Reveals target market/geography

Additional risk: PostgREST exposes an OpenAPI schema at /rest/v1/ when accessed with a valid API key. If the anon key is public (embedded in frontend), an attacker can discover every exposed table, column, and relationship in a single request.

Remediation:

  • Use Supabase Edge Functions or a backend API to proxy requests, hiding the direct PostgREST interface.
  • Restrict which tables are exposed to the public schema using PostgreSQL roles.
  • Consider using database views with limited columns instead of exposing raw tables.

MEDIUM — Missing Security Headers

Field Value
Severity Medium
Category OWASP A05:2021 — Security Misconfiguration

Present headers (good):

  • x-content-type-options: nosniff
  • strict-transport-security: max-age=31536000; includeSubDomains; preload

Missing headers:

Header Purpose Status
Content-Security-Policy Prevents XSS and data injection ❌ Missing
X-Frame-Options Prevents clickjacking ❌ Missing
X-XSS-Protection Legacy XSS filter ❌ Missing
Referrer-Policy Controls referrer leakage ❌ Missing
Permissions-Policy Restricts browser features ❌ Missing

Note: Some of these are less relevant for a pure API endpoint but become critical if any HTML responses are served.


MEDIUM — Storage Endpoint Information Disclosure

Field Value
Severity Medium
Category OWASP A04:2021 — Insecure Design

Observation:

GET /storage/v1/object/public/
→ 400: {"statusCode":"404","error":"Bucket not found","message":"Bucket not found"}

The storage endpoint returns a 400 with a "Bucket not found" message without requiring an API key, while all other endpoints return 401. This inconsistency means:

  1. An attacker can enumerate public bucket names by brute-forcing the path.
  2. If a public bucket exists with a guessable name (e.g., uploads, images, documents), its contents may be accessible without authentication.

Remediation:

  • Ensure no storage buckets are set to public unless absolutely necessary.
  • Use signed URLs for file access instead of public buckets.
  • Audit bucket names and access policies in the Supabase dashboard.

MEDIUM — Potential RLS Bypass Patterns

Field Value
Severity Medium–Critical (depending on configuration)
Category OWASP A01:2021 — Broken Access Control

Common Supabase RLS pitfalls that should be verified:

# Pattern Risk Check
1 RLS not enabled on table All data readable by anon role SELECT relname, relrowsecurity FROM pg_class WHERE relname = 'formas_recebimento';
2 Policy uses auth.uid() but table relates via company_id Users in same company OK, but no check if user belongs to that company Verify JWT claims include company_id
3 INSERT / UPDATE / DELETE policies missing RLS only on SELECT — attacker can still write/delete data Check policies for all operations
4 service_role key used in frontend Bypasses all RLS entirely Search frontend bundle for service_role
5 RLS policy uses current_setting() that can be set by client Attacker can inject role claims Audit policy definitions
6 Realtime subscriptions ignore RLS Supabase Realtime respects RLS since 2023, but verify Check Realtime channel configurations

Remediation: Run this audit query on your database:

-- Check which tables have RLS enabled
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;

-- Check existing RLS policies
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;

LOW — Verbose Error Messages

Field Value
Severity Low
Category OWASP A04:2021 — Insecure Design

Error responses include hints:

{
  "message": "No API key found in request",
  "hint": "No `apikey` request header or url param was found."
}

This tells an attacker:

  • The API expects an apikey header or URL parameter.
  • The parameter name is apikey.
  • This is a PostgREST/Supabase deployment.

Remediation: Consider using a reverse proxy that returns generic 401 responses.


Risk Summary

# Finding Severity OWASP Category
1 IDOR via client-side company_id filter Critical A01 — Broken Access Control
2 CORS allows all origins + all methods High A05 — Security Misconfiguration
3 Schema/data model leakage via URL High A04 — Insecure Design
4 Missing security headers Medium A05 — Security Misconfiguration
5 Storage endpoint info disclosure Medium A04 — Insecure Design
6 Potential RLS bypass patterns Medium–Critical A01 — Broken Access Control
7 Verbose error messages Low A04 — Insecure Design

Recommended Actions (Priority Order)

Immediate (Do Today)

  1. Enable RLS on ALL public tables — Run ALTER TABLE <table> ENABLE ROW LEVEL SECURITY; on every table.
  2. Create RLS policies scoped to auth.jwt() claims (e.g., company_id).
  3. Verify no service_role key is in frontend code — Search your frontend build for service_role.

Short-Term (This Week)

  1. Audit all RLS policies using the SQL queries above.
  2. Restrict CORS origins to your frontend domain(s).
  3. Review storage bucket policies — disable public access where not needed.
  4. Add write/delete RLS policies — not just SELECT.

Medium-Term (This Sprint)

  1. Add an API gateway or Edge Function proxy to hide direct PostgREST access.
  2. Implement rate limiting on auth endpoints to prevent credential stuffing.
  3. Add security headers via reverse proxy or CDN configuration.
  4. Set up Supabase audit logging to detect unauthorized access attempts.

Methodology

This audit was performed through external reconnaissance only, without:

  • Access to the Supabase dashboard
  • Knowledge of the anon or service_role keys
  • Authenticated requests to the API
  • Access to the application source code

Techniques used:

  • HTTP endpoint probing (status code analysis)
  • CORS header analysis
  • Security header audit
  • Error message analysis
  • Storage endpoint behavior testing
  • URL/query parameter analysis for access control patterns

A full internal audit with dashboard access would reveal significantly more about RLS policies, auth configuration, and storage security.


Report generated on 2026-03-25 — External security assessment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment