supabase-sicuro
Skill4 starsChanged 10 days agoMITView on GitHub
Mette in sicurezza un progetto Supabase (Postgres, RLS, Auth, Storage, Realtime). Regole di accesso su ogni tabella, permessi espliciti, funzioni SECURITY DEFINER, bucket, impostazioni di Auth, chiavi API e prove come utenti diversi. Trigger - "Supabase", "RLS", "policy", "migrazione", "anon key", "service role", "bucket", "row level security". EN - Secures a Supabase project (Postgres, RLS, Auth, Storage, Realtime) - access rules on every table, explicit grants, SECURITY DEFINER functions, buckets, Auth settings, API keys and tests as different users. Triggers - "Supabase", "RLS", "policy", "migration", "anon key", "service role", "storage bucket", "row level security".
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 3 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 133
check the caller with auth.uid() and the caller's rights on the data they touch.
- Very specific: line 141
Never dynamic SQL with client text.
- Very specific: line 151
Only the public key goes in the browser: today the *publishable* one (sb_publishable_…).
- Distinctive: line 170
if the app stores several sessions (account switching), the access tokens in the browser are worth as much as the password: no XSS, a …
- Distinctive: line 173
Test every rule both ways (allowed and denied), inside a transaction that always rolls back:
- Very specific: line 183
After every migration: no new warnings from the Security Advisor and the Performance Advisor.
- Distinctive: line 186
Never store exact locations unless needed: round them in the database (a trigger), not only in the app.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Supabase sicuro · Secure Supabase
🇮🇹 Versione italiana qui sotto · 🇬🇧 English version below
Con Supabase il browser parla direttamente con il database (PostgREST, RPC, Storage, Realtime) usando la chiave pubblica. Ogni tabella e ogni funzione è un'API pubblica: l'unica difesa sono le regole nel database. Chi conosce l'indirizzo del progetto e la chiave pubblica (sono nel codice del sito) può chiamare tutto ciò che le regole permettono.
Regole obbligatorie
1. RLS su ogni tabella, sempre
alter table … enable row level securitynella stessa migrazione che crea la tabella. Una tabella senza policy con RLS attiva non si legge: è il punto di partenza giusto.- Le policy dicono chi vede quali righe (
using) e cosa può scrivere (with check). Nelle insert e update, il proprietario si lega sempre aauth.uid()(with check (user_id = (select auth.uid()))), mai a un valore scelto dal client. - Le colonne privilegiate (verificato, piano, ruolo, contatori, stato di una richiesta) non si aggiornano dal client. Si usano permessi per colonna (
grant update (bio, city) on … to authenticated) o trigger che le rifiutano. - I dati altrui sensibili (data di nascita, posizione, email, telefono) si escludono con permessi per colonna, o si espongono solo tramite funzioni che restituiscono il minimo (l'età, una posizione arrotondata).
2. Permessi espliciti sulle tabelle nuove (dal 30 ottobre 2026)
Una tabella nuova nello schema public non è più esposta da sola all'API. Ogni migrazione che la crea deve dire chi può usarla:
grant select, insert, delete on public.commenti to authenticated; -- solo le azioni che servono
-- anon solo se la tabella si legge senza accessoSenza grant l'app riceve "permission denied", con il suggerimento del comando. Le tabelle esistenti non cambiano.
3. Funzioni
security invoker(il default) quando bastano le regole di chi chiama.- Le
security definer(girano con i privilegi del proprietario) devono:- avere
set search_path = ''e nomi completi (public.tabella); - controllare chi chiama con
auth.uid()e i suoi diritti sui dati toccati; - togliere l'esecuzione a tutti e ridarla solo a chi serve:
revoke execute on function … from public, anon, authenticated;poigrant execute on function … to authenticated;. Le funzioni interne vanno in uno schema non esposto:
- avere
create schema if not exists private;
revoke all on schema private from public, anon;
alter default privileges in schema private revoke execute on functions from public;
grant usage on schema private to authenticated; -- solo se le policy usano funzioni di private- Mai SQL dinamico con testo del client. Se serve, usare
format('%I', …)e%L.
4. Storage
- Bucket privati per default. Pubblici solo per immagini davvero pubbliche.
- Le policy di
storage.objectslegano il percorso al proprietario:(storage.foldername(name))[1] = (select auth.uid())::textper insert, update e delete. - Limitare tipi e dimensioni sul bucket (
allowed_mime_types,file_size_limit). Niente HTML o SVG caricati dagli utenti in un bucket pubblico: sarebbero pagine eseguibili sul dominio di Supabase. - Togliere i dati EXIF e GPS dalle foto nel browser (ridisegnarle su un canvas) come primo livello; un client modificato lo salta, quindi caricare in un bucket privato di quarantena e ricodificare lato server prima di pubblicare (privacy-posizione §6, sicurezza-social).
- Nei bucket pubblici l'URL è permanente e il percorso si vede:
<uid>/foto.jpgrivela l'id della persona. Nomi casuali, oppure bucket privati con URL firmati a scadenza breve.
5. Chiavi API
- Nel browser va solo la chiave pubblica: oggi la publishable (
sb_publishable_…). Le vecchieanoneservice_role(JWT) spariscono entro la fine del 2026: migrare e poi disattivarle in Settings → API Keys. - La chiave secret /
service_rolescavalca la RLS: solo su server o in Edge Functions, mai nel frontend né nella repo. Se finisce in un commit, va ruotata subito.
6. Auth
Nella dashboard controllare:
- conferma dell'email attiva;
- password lunghe: almeno 12 caratteri (8 è il minimo assoluto), controllate contro le password trapelate; le regole di composizione (lettere, numeri, simboli) sono facoltative, contano di più lunghezza e controllo (NIST SP 800-63B);
- protezione dalle password trapelate (piano Pro);
- scadenza breve dei codici OTP;
- cambio email sicuro, con conferma su entrambi gli indirizzi;
- Redirect URLs solo verso i propri domini, niente
*aperti; - limiti di frequenza su accessi, iscrizioni ed email;
- captcha (hCaptcha o Turnstile) sull'iscrizione, subito se l'app fa incontrare sconosciuti;
- cambio password sicuro (richiede un nuovo accesso recente);
- MFA (TOTP) disponibile, e obbligatoria per moderatori, staff e amministratori: le policy delle loro azioni chiedono
(select auth.jwt() ->> 'aal') = 'aal2'; - collegamento automatico delle identità (Google e email con lo stesso indirizzo): accettarlo solo con email verificate dal provider.
Nell'app:
- i messaggi non devono rivelare se un'email è registrata, quando questo conta (enumerazione);
- se l'app salva più sessioni (cambio account), le chiavi di accesso nel browser valgono quanto la password: niente XSS, CSP stretta.
7. Prove come utenti diversi
Ogni regola si prova in due direzioni (consentito e negato), dentro una transazione che si annulla:
do $$ begin
perform set_config('request.jwt.claims', json_build_object('sub', '<uuid B>', 'role', 'authenticated')::text, true);
perform set_config('role', 'authenticated', true);
-- B non deve vedere i dati privati di A:
if exists (select 1 from public.messaggi where autore = '<uuid A>' and privato) then raise exception 'FAIL'; end if;
raise exception 'DRY RUN OK'; -- annulla sempre
end $$;Dopo ogni migrazione: Security Advisor e Performance Advisor senza avvisi nuovi. Le prove più importanti diventano test automatici: pgTAP con supabase test db su un database locale, eseguito nella CI a ogni PR.
8. Privacy
- Posizioni mai esatte se non servono: arrotondarle nel database (trigger), non solo nell'app.
- Minimo indispensabile nelle risposte delle RPC.
- La cancellazione dell'account deve togliere davvero dati e file (diritto all'oblio).
9. Realtime
postgres_changesfiltra con la RLS gli inserimenti e le modifiche, ma gli eventi DELETE arrivano a tutti gli iscritti della tabella, con la chiave primaria (conreplica identity fullanche il resto della riga vecchia). Nella pubblicazionesupabase_realtimemettere solo tabelle i cui id non dicono nulla: mai presenze, blocchi, amicizie, posizioni.- Per dati personali in tempo reale usare canali privati (Broadcast o Presence con
config: { private: true }) e nelle impostazioni di Realtime togliere l'accesso pubblico, così esistono solo canali privati. - Chi può ascoltare o scrivere su un canale lo decidono le policy su
realtime.messages:
create policy "membri ascoltano" on realtime.messages for select to authenticated
using (realtime.messages.extension in ('broadcast', 'presence')
and private.puo_entrare(realtime.topic())); -- es. 'tavolo:<id>' → è membro?
create policy "membri scrivono" on realtime.messages for insert to authenticated
with check (private.puo_entrare(realtime.topic()));10. Piattaforma
- Settings → Database: SSL obbligatorio, Network Restrictions sulle connessioni dirette (solo gli IP che servono), password del database lunga e cambiata quando qualcuno esce dal team.
- Leva d'emergenza: in Settings → API si può spegnere la Data API (PostgREST); in Auth si chiudono le iscrizioni (risposta-incidenti).
- Viste e viste materializzate:
security_invoker = onsulle viste; le materializzate non hanno RLS, quindi in uno schema non esposto (api-sicura). - Backup e PITR verificati con un ripristino di prova; chi può scaricarli è un elenco breve.
Secure Supabase (English)
With Supabase the browser talks to the database directly (PostgREST, RPC, Storage, Realtime) with the public key. Every table and every function is a public API: the database rules are the only defense. Anyone who knows the project URL and the public key (both are in the site's code) can call whatever the rules allow.
Mandatory rules
1. RLS on every table, always
alter table … enable row level securityin the same migration that creates the table. A table with RLS on and no policy cannot be read: that is the right starting point.- Policies say who sees which rows (
using) and what they may write (with check). For inserts and updates, always tie the owner toauth.uid()(with check (user_id = (select auth.uid()))), never to a client-chosen value. - Privileged columns (verified, plan, role, counters, a request's status) are not client-updatable. Use column grants (
grant update (bio, city) on … to authenticated) or triggers that reject them. - Other people's sensitive data (birthdate, location, email, phone) is excluded with column grants, or exposed only through functions that return the minimum (the age, a rounded location).
2. Explicit grants on new tables (from 30 October 2026)
A new table in the public schema is no longer exposed to the API on its own. Every migration that creates one must say who can use it:
grant select, insert, delete on public.comments to authenticated; -- only the actions you need
-- anon only if the table is readable without signing inWithout a grant the app gets "permission denied", with a hint showing the command. Existing tables are unaffected.
3. Functions
security invoker(the default) when the caller's own rules are enough.security definerones (they run with the owner's privileges) must:- set
search_path = ''and use fully qualified names (public.table); - check the caller with
auth.uid()and the caller's rights on the data they touch; - drop execute from everyone and give it back only where needed:
revoke execute on function … from public, anon, authenticated;thengrant execute on function … to authenticated;. Internal functions belong in an unexposed schema:
- set
create schema if not exists private;
revoke all on schema private from public, anon;
alter default privileges in schema private revoke execute on functions from public;
grant usage on schema private to authenticated; -- only if policies call functions in private- Never dynamic SQL with client text. If needed, use
format('%I', …)and%L.
4. Storage
- Private buckets by default. Public only for truly public images.
storage.objectspolicies tie the path to the owner:(storage.foldername(name))[1] = (select auth.uid())::textfor insert, update and delete.- Restrict types and sizes on the bucket (
allowed_mime_types,file_size_limit). No user-uploaded HTML or SVG in a public bucket: they would be executable pages on Supabase's domain. - Strip EXIF and GPS from photos in the browser (redraw them on a canvas) as a first layer; a modified client skips it, so upload to a private quarantine bucket and re-encode server-side before publishing (privacy-posizione §6, sicurezza-social).
- In public buckets the URL is permanent and the path shows:
<uid>/photo.jpgreveals the person's id. Random names, or private buckets with short-lived signed URLs.
5. API keys
- Only the public key goes in the browser: today the publishable one (
sb_publishable_…). The oldanonandservice_role(JWT) keys go away by the end of 2026: migrate, then disable them in Settings → API Keys. - The secret /
service_rolekey bypasses RLS: server or Edge Functions only, never in the frontend or the repo. If it lands in a commit, rotate it at once.
6. Auth
In the dashboard, check:
- email confirmation on;
- long passwords: at least 12 characters (8 is the absolute minimum), checked against leaked passwords; composition rules (letters, digits, symbols) are optional, length and the leaked check matter more (NIST SP 800-63B);
- leaked password protection (Pro plan);
- short OTP expiry;
- secure email change, confirmed on both addresses;
- Redirect URLs only to your own domains, no open
*; - rate limits on sign-ins, sign-ups and emails;
- a captcha (hCaptcha or Turnstile) on sign-up, from day one if the app makes strangers meet;
- secure password change (requires a recent sign-in);
- MFA (TOTP) available, and mandatory for moderators, staff and admins: the policies of their actions require
(select auth.jwt() ->> 'aal') = 'aal2'; - automatic identity linking (Google and email with the same address): accept it only with emails verified by the provider.
In the app:
- messages must not reveal whether an email is registered, where that matters (enumeration);
- if the app stores several sessions (account switching), the access tokens in the browser are worth as much as the password: no XSS, a strict CSP.
7. Tests as different users
Test every rule both ways (allowed and denied), inside a transaction that always rolls back:
do $$ begin
perform set_config('request.jwt.claims', json_build_object('sub', '<uuid B>', 'role', 'authenticated')::text, true);
perform set_config('role', 'authenticated', true);
-- B must not see A's private data:
if exists (select 1 from public.messages where author = '<uuid A>' and private) then raise exception 'FAIL'; end if;
raise exception 'DRY RUN OK'; -- always rolls back
end $$;After every migration: no new warnings from the Security Advisor and the Performance Advisor. The key tests become automated: pgTAP with supabase test db on a local database, run in CI on every PR.
8. Privacy
- Never store exact locations unless needed: round them in the database (a trigger), not only in the app.
- Return the bare minimum from RPCs.
- Account deletion must really remove data and files (right to erasure).
9. Realtime
postgres_changesfilters inserts and updates through RLS, but DELETE events reach every subscriber of the table, with the primary key (withreplica identity full, the rest of the old row too). Only put tables whose ids say nothing into thesupabase_realtimepublication: never presence, blocks, friendships, locations.- For personal data in real time use private channels (Broadcast or Presence with
config: { private: true }) and turn off public access in the Realtime settings, so only private channels exist. - Who may listen or write on a channel is decided by policies on
realtime.messages:
create policy "members listen" on realtime.messages for select to authenticated
using (realtime.messages.extension in ('broadcast', 'presence')
and private.can_join(realtime.topic())); -- e.g. 'table:<id>' → is a member?
create policy "members write" on realtime.messages for insert to authenticated
with check (private.can_join(realtime.topic()));10. Platform
- Settings → Database: SSL enforced, Network Restrictions on direct connections (only the IPs that need them), a long database password changed whenever someone leaves the team.
- Emergency lever: in Settings → API you can turn off the Data API (PostgREST); in Auth you can close sign-ups (risposta-incidenti).
- Views and materialized views:
security_invoker = onon views; materialized views have no RLS, so keep them in an unexposed schema (api-sicura). - Backups and PITR verified with a test restore; a short list of who can download them.
In this repository14 other files it gives its agents.
- account-fornitoriSkill
- aggiornamenti-runtimeSkill
- api-sicuraSkill
- architettura-fortezzaSkill
- autenticazione-sicuraSkill
- difesa-attacchiSkill
- email-dominioSkill
- frontend-spaSkill
- hardening-sitiSkill
- privacy-pagamentiSkill
- privacy-posizioneSkill
- risposta-incidentiSkill
- sicurezza-githubSkill
- sicurezza-socialSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

