En bref
J’ai envoyé les mêmes requêtes à 11 jeux de règles Firestore, en tant qu’invité, utilisateur anonyme et trois utilisateurs connectés. request.auth != null a laissé un inconnu lire, modifier et supprimer les commandes de tout le monde. Un contrôle du propriétaire sur resource seul a permis à un utilisateur de céder une commande à quelqu’un d’autre. Un seul match large {document=**} a ouvert une collection qui avait pourtant une règle de propriétaire correcte. Et un rôle stocké dans un document que l’utilisateur peut écrire l’a rendu admin en une seule requête. Chaque cas a sa correction ci-dessous, et la règle corrigée a tenu face aux mêmes requêtes.
Une application Firebase n’a aucun serveur à vous entre le navigateur et la base de données. La configuration du projet est livrée dans la page, et les règles de sécurité sont tout ce qui sépare ceux qui la possèdent de vos données. Je voulais donc des exemples de règles avec l’attaque à côté, pas une liste de règles qui ont l’air correctes.
Comment j’ai testé. Firebase Local Emulator Suite de Firebase CLI 15.34.2, le 3 novembre 2026. J’ai chargé chaque jeu de règles, réinitialisé les données et appelé l’API REST de Firestore avec de vrais jetons de l’émulateur Auth : aucun jeton, un utilisateur anonyme, les utilisateurs A et B, et un inconnu qui venait seulement de s’inscrire. Les données étaient inventées : les commandes a1 et a2 appartenant à A, b1 appartenant à B.
Quelles règles Firebase fuient ?
| Règle | Requête | Résultat |
|---|---|---|
if true (mode test) |
L’invité liste les commandes | 200, les 3 |
if true (mode test) |
L’invité supprime la commande de B | 200 |
request.auth != null |
L’utilisateur anonyme liste les commandes | 200, les 3 |
request.auth != null |
L’inconnu supprime la commande de A | 200 |
Contrôle du propriétaire sur resource |
A crée une commande appartenant à B | 200 |
Contrôle du propriétaire sur resource |
A modifie ownerId de sa propre commande |
200 |
Règle propriétaire plus lecture {document=**} |
A lit la commande de B | 200 |
L’utilisateur écrit son propre doc users |
A met role: admin, lit la commande de B |
200 |
Que ouvre vraiment le mode test ?
match /{document=**} {
allow read, write: if true;
}
Tout, à tout le monde. Un invité sans jeton a listé les 3 commandes, en a supprimé une et en a créé une nouvelle. Rien de tout cela ne demande un compte ou un secret divulgué, seulement l’identifiant du projet.
La version datée que le mode test utilise souvent échoue autrement. Avec if request.time < timestamp.date(2026, 1, 1) et cette date dans le passé, l’invité a reçu 403, et l’utilisateur A aussi en lisant sa propre commande. La fuite est terminée, et l’application aussi.
request.auth != null suffit-il ?
Non. Cela dit seulement qu’un compte quelconque se trouve derrière la requête.
match /orders/{id} {
allow read, write: if request.auth != null;
}
L’invité a reçu 403, ce qui explique pourquoi cette règle paraît sûre dans un test rapide. Puis un inconnu qui venait de s’inscrire a listé les 3 commandes, changé le total de celle de B et supprimé celle de A, chaque fois avec 200. Un utilisateur anonyme a lui aussi listé les 3. Si l’inscription est ouverte, cette règle signifie « tous ceux qui se donnent la peine de s’inscrire ». La documentation de Firebase la cite aussi parmi les schémas dangereux courants.
Pourquoi un contrôle du propriétaire doit-il porter sur resource et request.resource ?
resource est le document tel qu’il est stocké. request.resource est le document tel qu’il serait après l’écriture. Une règle qui ne regarde que le premier vérifie à qui appartient la ligne maintenant, et ne dit rien de ce qu’on en fait.
match /orders/{id} {
allow read, update, delete: if resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null;
}
Les lectures semblaient correctes : A a reçu 200 sur sa commande et 403 sur celle de B. Pas les écritures. A a créé une commande avec ownerId réglé sur B, et a reçu 200. A a ensuite changé ownerId de sa propre commande pour B, également 200, et B pouvait la lire à partir de là. Dans une application où les commandes sont des factures ou des soldes, c’est un utilisateur qui écrit dans le compte de quelqu’un d’autre.
Cette version a tout refusé :
match /orders/{id} {
allow read, delete: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.ownerId == request.auth.uid;
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.ownerId == resource.data.ownerId;
}
| Requête de A | Résultat |
|---|---|
| Lire sa propre commande | 200 |
| Lire, modifier, supprimer celle de B | 403 |
| Créer une commande appartenant à B | 403 |
Changer ownerId pour B |
403 |
A pouvait encore modifier le total de sa propre commande, avec 200. Si le client ne doit pas fixer ce champ, dites quels champs il peut modifier :
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['note']);
Avec cette règle, modifier total a renvoyé 403 et modifier note a renvoyé 200.
Pourquoi ma requête a-t-elle échoué avec une règle correcte ?
Parce que les règles ne sont pas des filtres. Avec la règle de propriétaire ci-dessus, A qui demandait toute la collection a reçu 403, alors qu’il possède 2 des 3 commandes. Firestore juge la requête d’après ce qu’elle pourrait renvoyer, pas d’après ce qui existe.
| Requête de A | Résultat |
|---|---|
| Lister les commandes | 403 |
Requête ownerId == A |
200, 2 docs |
Requête ownerId == B |
403 |
Donc quand une liste casse après un durcissement des règles, corrigez la requête. Assouplir la règle pour que la liste marche, c’est ainsi que s’écrivent les règles trop larges vues plus haut.
Un match large peut-il annuler une règle correcte ?
Oui, et c’est le premier point que je chercherais dans un projet avec un long fichier de règles. Firestore autorise une requête si une seule règle correspondante l’autorise.
match /orders/{id} {
allow read: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
}
match /{document=**} {
allow read: if request.auth != null;
}
Le premier bloc ressemble à une bonne règle de propriétaire. Avec le second en place, A a lu la commande de B avec 200, un inconnu a listé les 3 commandes, et le même inconnu a lu le document de A dans users. Rien dans le premier bloc ne vous avertit. Supprimez le match large ; aucune règle placée plus bas ne peut reprendre l’accès.
Comment un utilisateur se rend-il admin ?
Ce schéma apparaît dès que les rôles vivent dans un document :
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
match /users/{uid} {
allow read, write: if request.auth.uid == uid;
}
match /orders/{id} {
allow read: if resource.data.ownerId == request.auth.uid || isAdmin();
}
« Les utilisateurs peuvent modifier leur propre profil » et « les admins peuvent lire toutes les commandes » sont chacune raisonnables. Ensemble, elles forment une élévation de privilèges.
La correction est d’empêcher le client d’écrire ce champ. Avec request.resource.data.role == resource.data.role ajouté à la règle d’écriture, la même requête a renvoyé 403 et A est resté hors de la commande de B. Les custom claims posés depuis un serveur sont l’autre solution, puisqu’un client ne peut pas les modifier.
Deux choses qui se sont avérées correctes
Les règles d’un parent ne fuient pas dans les sous-collections. Avec allow read sur /orders/{id} pour tout utilisateur connecté, A a lu la commande, et /orders/b1/notes/n1 en dessous a renvoyé 403. C’est le comportement documenté : les règles ne s’appliquent qu’au chemin qui correspond.
Un invité ne passe pas à travers une comparaison avec un jeton absent. resource.data.ownerId == request.auth.uid a renvoyé 403 pour l’invité, même sans request.auth != null séparé. J’écris quand même le contrôle, parce que la règle dit alors ce qu’elle veut dire.
Comment vérifier vos propres règles ?
Lancez vos requêtes dans l’émulateur en tant qu’invité, qu’inconnu et que second utilisateur. C’est ce qui a révélé chacun des cas ci-dessus ; lire les règles ne l’a pas fait.
Pour un premier passage, le Vérificateur de règles Firebase gratuit lit des règles collées sans toucher à votre projet. Sur les jeux de règles de cet article, il a signalé le mode test, l’accès réservé aux utilisateurs connectés, le match large et le rôle écrit par l’utilisateur. Il ne lit que du texte : il ne peut pas vous dire à quoi ressemblent vos données ni si une règle a été déployée.
Votre apiKey dans la page n’est pas le problème en soi. La configuration web de Firebase est publique par conception, et le scanner de secrets gratuit la signale comme telle, car ce sont les règles qui protègent les données derrière. Les fichiers suivent encore d’autres règles, avec une fuite que les règles ne peuvent pas arrêter, que j’ai testée dans les règles Firebase Storage.
Limites. Firestore uniquement, dans l’émulateur local, via REST, sur des données inventées. Realtime Database utilise un autre langage de règles et n’a pas été testée. App Check, qui limite les applications autorisées à appeler votre backend, n’en fait pas partie ; il ne remplace pas les règles. La production peut différer de l’émulateur sur des détails que je n’ai pas vérifiés.
Vous ne savez pas ce que vos règles laissent passer ?
Je les lirai volontiers avec vous. Sans démarchage.
Me contacter