Vérificateur de clé Supabase

Deux clés identiques à l'oeil, des conséquences opposées.

Collez une clé Supabase et découvrez laquelle vous tenez. Rien n'est transmis : la clé est décodée dans votre navigateur, parce qu'envoyer un identifiant de base de données en production à un site web pour vérification serait exactement l'erreur que cet outil existe pour attraper.

Tourne dans votre navigateur. Rien ne nous est envoyé.

Ce que cela vérifie, et à quoi ressemble une bonne réponse

Pourquoi les deux clés sont impossibles à distinguer à l'oeil

Toutes deux sont des JSON Web Tokens. Toutes deux ont la même longueur, commencent par les mêmes caractères et se copient depuis deux lignes voisines du même écran de réglages. La seule différence tient à un claim enfoui dans la section centrale : role, qui vaut soit anon, soit service_role. Cette section est du base64, pas du chiffrement : n'importe qui peut la lire. Cet outil se contente de la lire pour vous.

service_role : contourne chaque règle que vous avez écrite

Cette clé ignore entièrement le row level security. Pas "dispose de larges permissions" : ignore les règles. Chaque règle comparant une ligne à auth.uid() est sautée, si bien que le porteur lit, modifie et supprime chaque ligne de chaque table comme si les règles n'avaient jamais été écrites. Si elle est un jour apparue dans du code client, un dépôt public, une capture d'écran ou un ticket de support, considérez-la comme déjà compromise et faites-la tourner. Les collecteurs automatiques trouvent des identifiants dans les bundles publics en quelques heures.

anon : publique par conception, et aussi sûre que vos règles

La clé anon est faite pour partir dans les navigateurs : la trouver dans votre JavaScript n'est donc pas un problème. Ce qu'elle vaut dépend entièrement de votre row level security. Avec le RLS activé et des règles qui comparent les lignes à l'utilisateur connecté, elle est exactement ce qu'elle doit être. Avec le RLS désactivé sur une table, ou une règle qui dit using (true), cette clé publique lit toute cette table pour quiconque a chargé votre site, c'est-à-dire pour tout le monde.

Comment cela dérape en pratique

Presque toujours de la même façon. Quelque chose renvoie un tableau vide ou une erreur de permission, le plus rapide pour que ça cesse est l'autre clé, et ça marche immédiatement parce que cette clé ignore la règle qui vous arrêtait à juste titre. L'erreur, c'était le système qui fonctionnait. Échanger les clés ne corrige pas un problème d'accès, cela supprime le contrôle d'accès, et cela part généralement en production parce que rien ne casse visiblement ensuite.

Le claim d'expiration

Les clés Supabase portent un exp. Nous le signalons parce qu'une clé expirée explique toute une catégorie de panne déroutante en production, mais notez que l'expiration ne répare pas une exposition : une clé service_role qui a fuité puis expiré est restée valide pendant toute la fenêtre où elle était publique.

Comment corriger

S'il s'agit d'une clé anon, il n'y a rien à corriger ici et le travail est dans vos règles. S'il s'agit d'une service_role qui est passée près d'un navigateur, faites-la tourner d'abord et enquêtez ensuite.

Activez le RLS et écrivez une vraie règle
-- A table without this is readable by anyone holding the anon key,
-- which is everyone who has loaded your site.
alter table public.documents enable row level security;

-- Scope every row to its owner. "using" filters reads;
-- "with check" constrains writes. A policy with only "using" lets
-- someone insert rows they will not be able to see afterwards.
create policy "owners read their documents"
  on public.documents for select
  using (auth.uid() = user_id);

create policy "owners write their documents"
  on public.documents for insert
  with check (auth.uid() = user_id);
Trouvez les tables dont le RLS est encore désactivé
-- Run this in the SQL editor. Every row it returns is a table the
-- anon key can read in full.
select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;

-- And this one, for the policy that looks present but grants
-- everything to everyone:
select tablename, policyname, qual
from pg_policies
where schemaname = 'public'
  and qual = 'true';
Où va chaque clé
anon key
  -> client code is fine. VITE_SUPABASE_ANON_KEY,
     NEXT_PUBLIC_SUPABASE_ANON_KEY and friends are correct.

service_role key
  -> server only. An edge function, a server route, or your host's
     secret store. Never in a variable your bundler inlines, which
     means never behind NEXT_PUBLIC_, VITE_, PUBLIC_ or REACT_APP_.

# Check what you actually shipped:
grep -rE "service_role|SERVICE_ROLE" ./dist ./build ./.next 2>/dev/null

# And check the history, because deleting a key from a file does
# not delete it from the repository:
git log -p -S 'service_role' -- . | head -50
Faire tourner
1. Supabase dashboard -> Project Settings -> API -> rotate.
2. Update the secret in your host and redeploy the server side.
3. Confirm nothing still reads the old value, then revoke it.
4. Read the logs for the exposure window. A key that was public
   for a week deserves a look at what it was used for, not just
   a replacement.
Ce que cet outil ne peut pas voir

Ceci lit une clé que vous avez déjà. Cela ne peut pas vous dire si elle se trouve dans votre bundle de production, si elle est dans votre historique git, ni si les tables qu'elle atteint ont des règles dignes de ce nom. Pour le premier point, lancez le scanner de clés d'API exposées sur votre URL déployée. Pour les deux autres, la réponse est dans le dépôt et non dans quoi que ce soit de visible depuis l'extérieur.

Scanner aussi le codeGratuit pour démarrer. Connectez un dépôt et le premier scan tourne en deux minutes environ.