Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

10.5 — Sécurité dans un BaaS

🎯 Objectif : configurer une sécurité réelle dans un BaaS. Le piège n°1 : se croire sécurisé parce qu’on utilise une plateforme renommée.

À l'issue de cet axe, tu sauras :

  • Écrire des Row-Level Security (RLS) en PostgreSQL/Supabase
  • Configurer des Firestore Security Rules sans data leak
  • Activer App Check pour bloquer les bots et abus
  • Distinguer clé publique (anon) et clé secrète (service role)
  • Auditer la sécurité de son BaaS avec une check-list 2026

Confirmé 10 min prérequis : axes 8-9 lus

Aucun BaaS n’est sécurisé par défaut. Tu dois explicitement configurer les règles d’accès. Sans ça, ta DB est publique.

-- DANGER : table créée sans RLS activée
CREATE TABLE tasks (...);

Toute personne avec ta clé anon peut tout lire et tout écrire. Et la clé anon est dans ton frontend (donc publique).

Fix :

ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own tasks" ON tasks
FOR SELECT USING (auth.uid() = owner_id);
allow read, write: if true;

Toutes tes données sont publiques. Les bots scannent les projets Firebase et copient les DB exposées.

Fix :

match /tasks/{taskId} {
allow read, write: if request.auth != null
&& resource.data.owner_id == request.auth.uid;
}

C’est la sécurité au niveau base : Postgres lui-même refuse de retourner des lignes que l’utilisateur ne devrait pas voir.

ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

Une fois activée, sans policy : personne ne voit rien (sauf service_role). Tu dois écrire des policies pour autoriser.

CREATE POLICY "Users see own tasks" ON tasks
FOR SELECT
USING (auth.uid() = owner_id);

auth.uid() est une fonction Supabase qui retourne l’UUID du user connecté (extrait du JWT).

CREATE POLICY "Users create own tasks" ON tasks
FOR INSERT
WITH CHECK (auth.uid() = owner_id);

USING filtre les lignes existantes ; WITH CHECK valide les nouvelles lignes insérées.

CREATE POLICY "Users update own tasks" ON tasks
FOR UPDATE
USING (auth.uid() = owner_id)
WITH CHECK (auth.uid() = owner_id);

Les deux : tu ne peux update que tes lignes (USING) ET tu ne peux pas changer le owner_id pour voler la ligne d’un autre (WITH CHECK).

CREATE POLICY "Users delete own tasks" ON tasks
FOR DELETE
USING (auth.uid() = owner_id);
-- Admin peut tout voir
CREATE POLICY "Admins see all" ON tasks
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM users WHERE id = auth.uid() AND role = 'admin'
)
);
-- Membres d'une équipe peuvent voir les tâches partagées
CREATE POLICY "Team members see shared tasks" ON tasks
FOR SELECT
USING (
team_id IN (
SELECT team_id FROM team_members WHERE user_id = auth.uid()
)
);

Indispensable. Avec pgTAP ou un script qui crée 2 users et vérifie que A ne voit pas les données de B.

-- Bascule en tant que user A
SET LOCAL request.jwt.claims = '{"sub":"user-a-uuid"}';
SELECT * FROM tasks; -- doit voir ses tâches uniquement

Le DSL propriétaire de Firebase. Plus opaque que le SQL mais expressif.

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... règles par collection
}
}
match /tasks/{taskId} {
allow read: if isOwner(resource);
allow create: if request.auth != null
&& request.resource.data.owner_id == request.auth.uid
&& validTaskData(request.resource.data);
allow update: if isOwner(resource)
&& request.resource.data.owner_id == resource.data.owner_id;
allow delete: if isOwner(resource);
}
function isOwner(doc) {
return request.auth != null && doc.data.owner_id == request.auth.uid;
}
function validTaskData(data) {
return data.title is string
&& data.title.size() > 0
&& data.title.size() <= 200
&& data.done is bool;
}
VariableSens
request.authnull si non connecté, sinon { uid, token }
request.resource.dataLe document proposé (insert/update)
resource.dataLe document existant (read/update/delete)
request.timeTimestamp
request.methodget, list, create, update, delete
Fenêtre de terminal
firebase emulators:start --only firestore

Puis avec @firebase/rules-unit-testing :

import { initializeTestEnvironment, assertSucceeds, assertFails } from '@firebase/rules-unit-testing';
const env = await initializeTestEnvironment({
projectId: 'test',
firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') },
});
const alice = env.authenticatedContext('alice').firestore();
const bob = env.authenticatedContext('bob').firestore();
await assertSucceeds(alice.doc('tasks/x').set({ owner_id: 'alice', title: 'T' }));
await assertFails(bob.doc('tasks/x').get()); // Bob ne doit pas pouvoir lire

Firebase App Check (ou équivalent reCAPTCHA Enterprise sur Supabase) vérifie que la requête vient bien de ton app (web, iOS, Android), pas d’un script malveillant.

Sans App Check : un bot peut faire 1 million d’appels à ton Firebase / Supabase, drainer ton budget, scraper ta DB.

Avec App Check : chaque requête doit présenter un token cryptographique signé par ton app. Les bots passent leur chemin.

// Activer App Check côté client
import { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check';
initializeAppCheck(app, {
provider: new ReCaptchaV3Provider('site_key'),
isTokenAutoRefreshEnabled: true,
});
CléPublic ?Usage
SUPABASE_ANON_KEYCôté client, soumis au RLS
SUPABASE_SERVICE_ROLE_KEYCôté serveur, bypass RLS — danger absolu si exposée
STRIPE_PUBLISHABLE_KEY (pk_...)Affichage formulaire paiement
STRIPE_SECRET_KEY (sk_...)Côté serveur uniquement
CLERK_PUBLISHABLE_KEYCôté client
CLERK_SECRET_KEYCôté serveur
.env
# Clés publiques (préfixe NEXT_PUBLIC_ → exposées au client)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Clés secrètes (PAS de préfixe NEXT_PUBLIC_ → côté serveur uniquement)
SUPABASE_SERVICE_ROLE_KEY=eyJ...
STRIPE_SECRET_KEY=sk_test_...
RESEND_API_KEY=re_...
Fenêtre de terminal
# Compile et vérifie ce qui apparaît dans le bundle client
npm run build
grep -r "STRIPE_SECRET" .next/static/ # ne doit RIEN trouver
grep -r "service_role" .next/static/ # ne doit RIEN trouver

Outils : git-secrets, gitleaks, truffleHog en pre-commit hook.

Vérification
☐ RLS activée sur toutes les tables sensibles
☐ Tests automatiques des RLS (un user n’accède pas aux données d’un autre)
☐ Aucune clé secrète dans le bundle client (vérification automatique en CI)
☐ App Check / reCAPTCHA activé pour les endpoints publics
☐ Rate limiting sur /login, /register, /reset-password
☐ Webhooks signés vérifiés cryptographiquement
☐ Secrets stockés dans le secret manager du provider, pas en config
☐ MFA / Passkeys disponibles pour les utilisateurs sensibles
☐ Audit log des actions admin (qui a fait quoi quand)
☐ Monitoring des accès anormaux (Sentry, datadog)
Tu crées une table 'orders' dans Supabase sans activer RLS. Conséquence ?
Différence entre `USING` et `WITH CHECK` dans une policy Postgres ?
Tu vois `SUPABASE_SERVICE_ROLE_KEY=eyJ...` dans le bundle JavaScript de ton site (vérifié avec `npm run build` puis grep). Action ?

Fin de l’axe 10. Direction l’axe 11 — Qualité & tests, ou attaque le projet SaaS minimal en un week-end.