---
title: "Supabase raw_user_meta_data : une inscription devient Owner"
description: "Un trigger handle_new_user lisait role_id dans raw_user_meta_data, fixé par le navigateur. Comment tout le monde devenait Owner, la correction, un test."
url: "https://madsalt.tech/fr/notes/supabase-raw-user-meta-data-role/"
author: "Denys Zaichenko"
language: "fr"
published: 2026-10-18
---

# Supabase raw_user_meta_data : une inscription devient Owner

En bref

`raw_user_meta_data` contient ce que le navigateur a passé à `supabase.auth.signUp()`. Sur une application en production, le trigger `handle_new_user` y lisait le rôle du nouvel utilisateur ; n'importe qui pouvait donc s'inscrire avec l'identifiant du rôle Owner et prendre le contrôle du compte. La correction : le rôle vient d'une table que seul le serveur écrit, remplie avant l'envoi de l'invitation.

C'était sur le [suivi du temps d'un cabinet d'audit](https://madsalt.tech/fr/work/audit-firm-time-tracking/), une application Supabase où chaque règle d'accès vit dans la base de données. Le bug se trouvait dans le trigger qui crée un profil pour chaque nouvel utilisateur.

## Que faisait le trigger ?

Simplifié, il ressemblait à ceci :

```sql
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
declare
  _role_id uuid := (new.raw_user_meta_data ->> 'role_id')::uuid;
begin
  insert into public.profiles (id, full_name, role_id)
  values (new.id, new.raw_user_meta_data ->> 'full_name', _role_id);
  return new;
end;
$$;
```

Le flux d'invitation y mettait le bon `role_id`, donc en usage normal tout fonctionnait. Mais `raw_user_meta_data` correspond aux `options.data` d'une inscription, et c'est le navigateur qui décide de ce qu'il contient :

```js
await supabase.auth.signUp({
  email: 'me@example.com',
  password: '...',
  options: { data: { role_id: '<owner role uuid>' } },
})
```

Le contrôle de permission renvoyait vrai pour les owners avant de regarder quoi que ce soit d'autre, de sorte que ce seul champ donnait le contrôle total du compte. Le [guide Users](https://supabase.com/docs/guides/auth/users) de Supabase indique lui-même que les métadonnées utilisateur ne doivent pas servir dans les politiques RLS ni dans la logique d'autorisation, car l'utilisateur peut les modifier sans aucun contrôle.

![D'où venait le rôle d'un nouvel utilisateur. Le trigger handle_new_user avant et après la correction. Avant : toute personne qui s'inscrit: signUp options.data → raw_user_meta_data → handle_new_user → Rôle Owner; Après : seulement les invités: Fonction d'invitation → pending_invites → handle_new_user → Rôle invité.](https://madsalt.tech/charts/fr/supabase-raw-user-meta-data-role.svg)

*Issu du trigger d'inscription d'une application Supabase en production, juin 2026.*

## Qu'est-ce qui l'a corrigé ?

Le rôle vient maintenant d'une table privée que le client ne peut pas toucher :

```sql
create table private.pending_invites (
  email text primary key,
  role_id uuid,
  invited_by uuid,
  created_at timestamptz default now()
);
revoke all on table private.pending_invites from anon, authenticated;
```

La fonction d'invitation s'exécute côté serveur, après vérification de la permission de la personne qui invite. Elle écrit d'abord l'invitation en attente, puis seulement crée le lien d'invitation. Le trigger retrouve le rôle par l'e-mail et supprime la ligne. Le nom vient toujours des métadonnées, ce qui ne pose pas de problème : ce sont des données d'affichage.

L'ordre compte. Une invitation crée l'utilisateur immédiatement : le [guide Users](https://supabase.com/docs/guides/auth/users) de Supabase précise qu'inviter une nouvelle adresse e-mail crée un utilisateur non confirmé. Le trigger se déclenche à ce moment-là, la ligne en attente doit donc déjà exister.

## Comment vérifier votre propre application ?

Ouvrez votre fonction `handle_new_user`, ou n'importe quel trigger sur `auth.users`, et cherchez `raw_user_meta_data`. Le nom et l'avatar viennent de là sans souci. Le rôle, l'entreprise, l'offre, le locataire ou tout ce qui donne un accès doit venir d'une table ou de `raw_app_meta_data`, que l'utilisateur ne peut pas modifier.

Essayez ensuite : inscrivez-vous via l'API avec `role_id` ou `role: 'admin'` dans `options.data`. Le nouveau compte ne doit rien obtenir en plus.

Ce type de vérification est la première étape de ma [méthode de test de la RLS](https://madsalt.tech/fr/blog/how-to-test-supabase-rls/). Pour les applications multi-tenant, lisez aussi [ce que la RLS laisse passer pour un utilisateur dans deux organisations](https://madsalt.tech/fr/notes/supabase-rls-user-in-two-organizations/). Les autres vérifications que je lance sur chaque application Supabase sont dans ma [checklist de sécurité d'une application Lovable](https://madsalt.tech/fr/blog/lovable-app-security-checklist/). La première que je regarde dans le Dashboard est [quels schémas la Data API expose](https://madsalt.tech/fr/notes/supabase-exposed-schemas/). L'[Audit de sécurité Supabase](https://madsalt.tech/fr/tools/supabase-security-check/) couvre les rôles, droits, politiques et paramètres d'authentification avec 46 règles OWASP Top 10:2025, pas ce qu'un trigger copie des métadonnées d'inscription : cette vérification reste donc manuelle.
