Documentation

Guide Sécurité — Pixel Night

Modèle de menaces

Acteurs et niveaux de confiance

ActeurNiveau de confianceAccès
Organisateur authentifiéÉlevéAdmin panel, toutes les actions de gestion
Participant anonymeFaibleVote et propositions uniquement
Visiteur non authentifiéNulPages publiques en lecture
Attaquant externeHostileAucun accès prévu

Scénarios de menace

ScénarioProbabilitéMitigation en place
Double voteMoyennevoter_id unique par soiree en DB + contrainte UNIQUE
Prise de contrôle adminFaibleAuth Supabase + RLS, session cookie httpOnly
Exfiltration token TMDbFaibleStocké uniquement en variable d'environnement serveur
Injection SQLFaibleORM Supabase (requêtes paramétrées)
XSSFaibleReact escape natif, CSP headers Vercel
CSRFFaibleTokens Supabase + SameSite cookies
Scraping votes concurrentsPossibleContrainte UNIQUE DB (idempotent)

Authentification

Supabase Auth

  • Mécanisme : Email + Password, sessions JWT
  • Stockage : cookies httpOnly, SameSite=Lax, Secure (HTTPS)
  • Rafraîchissement : proxy.ts gère le refresh automatique des tokens via @supabase/ssr
  • Logout : invalide la session côté Supabase + supprime les cookies

Vérification côté serveur

Chaque route admin effectue :

const { data: { user } } = await authSupabase.auth.getUser()
if (!user) return NextResponse.json({ error: "Non autorise" }, { status: 401 })

getUser() valide le JWT auprès de Supabase (appel réseau) — pas de décodage local non sécurisé.

Autorisation — Row Level Security (RLS)

Les tables Supabase utilisent des politiques RLS pour restreindre les accès.

Pourquoi le service role est utilisé pour les votes

Les participants sont anonymes — ils n'ont pas de compte Supabase. RLS ne peut pas les authentifier via auth.uid(). Les route handlers de vote utilisent donc le service role (clé SUPABASE_SERVICE_ROLE_KEY) qui bypass RLS.

La logique de sécurité est alors implémentée dans le code du route handler :

  1. Vérifier que voter_id n'a pas déjà voté (SELECT ... WHERE voter_id = ?)
  2. Insérer le vote
  3. La contrainte UNIQUE(soiree_id, voter_id) en DB est le dernier filet de sécurité

Politiques RLS par table

TableLectureÉcritureNotes
sp_soireesPubliqueauth.uid() = created_by
sp_themesPubliqueauth.uid() = created_by
sp_soiree_filmsPubliqueService role
sp_soiree_film_proposalsPubliqueService role
sp_theme_votesService roleService rolePas de lecture directe
sp_film_votesService roleService role
sp_sallesauth.uid() = created_byauth.uid() = created_by
sp_profilesauth.uid() = idauth.uid() = id

Token TMDb

Le token TMDb est stocké exclusivement dans la variable d'environnement TMDB_API_READ_ACCESS_TOKEN (côté serveur uniquement). Il n'est jamais stocké en base de données.

Anonymat des votants

  • Identifiant voter_id : UUID v4 généré côté client, stocké dans localStorage
  • Aucune donnée personnelle associée : pas d'IP, pas de fingerprint, pas de cookie traceur
  • Lier un vote à une personne réelle est impossible sans accès physique au navigateur
  • Limitation : vider le localStorage ou naviguer en privé permet techniquement de voter deux fois

Sécurité des variables d'environnement

VariableExpositionRisque si compromise
NEXT_PUBLIC_SUPABASE_URLPublique (bundle client)Faible — URL publique
NEXT_PUBLIC_SUPABASE_ANON_KEYPublique (bundle client)Faible — limitée par RLS
SUPABASE_SERVICE_ROLE_KEYServeur uniquementCritique — bypass RLS complet
TMDB_API_READ_ACCESS_TOKENServeur uniquementMoyen — utilisation TMDb abusive

Bonnes pratiques

  • Ne jamais commiter .env.local (vérifier .gitignore)
  • Utiliser les secrets Vercel pour les variables sensibles
  • Rotation périodique de SUPABASE_SERVICE_ROLE_KEY si compromission suspectée

Protection contre les abus

Double vote

Deux mécanismes en cascade :

  1. SELECT avant insert dans le route handler → réponse 409 immédiate
  2. Contrainte UNIQUE(soiree_id, voter_id) en DB → error.code === "23505"409

Propositions de films

  • Max 3 propositions par voter_id par soirée — vérifiée côté serveur
  • Validation TMDb optionnelle : le détail du film est récupéré depuis TMDb avant insertion

Rate limiting

Pas de rate limiting applicatif actuellement. En production, Vercel Pro inclut un WAF basique. Pour un rate limiting applicatif, envisager @upstash/ratelimit avec Redis Upstash.

Headers de sécurité

Vercel applique automatiquement :

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block

Pour ajouter des headers custom (Content-Security-Policy), modifier next.config.mjs :

const nextConfig = {
  async headers() {
    return [{
      source: "/(.*)",
      headers: [{
        key: "Content-Security-Policy",
        value: "default-src 'self'; img-src 'self' image.tmdb.org data:; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net cdnjs.buymeacoffee.com"
      }]
    }]
  }
}

Audit de sécurité rapide

# Dépendances vulnérables
pnpm audit

# Variables d'env non commités
git log --all --full-history -- .env*

# Vérifier l'absence de secrets dans le code
grep -r "eyJ" --include="*.ts" --include="*.tsx" . --exclude-dir=node_modules