docs: ajouter ONBOARDING, SECURITY, CHANGELOG, ADR 001-006
This commit is contained in:
parent
28c9c08d31
commit
52b8e9d011
9 changed files with 1380 additions and 0 deletions
388
docs/SECURITY.md
Normal file
388
docs/SECURITY.md
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
# SECURITY.md — Expria Frontend
|
||||
|
||||
> **Document de référence — Version 1.0**
|
||||
> Ce document recense les exigences de sécurité du frontend Expria, les patterns interdits, les trous identifiés, et les procédures de réponse aux incidents.
|
||||
>
|
||||
> **Règle fondamentale :** la sécurité n'est pas une feature qu'on ajoute à la fin. C'est une contrainte présente à chaque commit.
|
||||
|
||||
---
|
||||
|
||||
## 1. Principes directeurs
|
||||
|
||||
### 1.1 Défense en profondeur
|
||||
|
||||
Le frontend est la couche la plus exposée. Il ne peut **pas** être la seule ligne de défense.
|
||||
|
||||
- **Couche 1 (prévention)** : TypeScript strict, plugins de sécurité Claude Code, ESLint security rules, scan Semgrep.
|
||||
- **Couche 2 (détection)** : CI GitHub Actions avec `npm audit`, Dependabot, GitHub secret scanning.
|
||||
- **Couche 3 (autorité)** : le backend est l'arbitre final de toute permission, tout quota, toute action.
|
||||
|
||||
Si une couche tombe, les autres tiennent.
|
||||
|
||||
### 1.2 Le frontend n'est pas de confiance
|
||||
|
||||
Un utilisateur malveillant peut :
|
||||
- Modifier le JavaScript du navigateur (via DevTools)
|
||||
- Appeler directement l'API backend sans passer par le frontend
|
||||
- Injecter des valeurs dans les requêtes
|
||||
|
||||
Conséquence : **toute vérification côté frontend est uniquement de l'UX** (masquer un bouton, afficher un cadenas). La vraie protection est dans les middlewares backend (`authMiddleware`, `planMiddleware`).
|
||||
|
||||
### 1.3 Transparence avec l'utilisateur
|
||||
|
||||
- Pas de collecte de données non nécessaires.
|
||||
- Pas d'analytics sans consentement explicite.
|
||||
- Messages d'erreur clairs, sans jargon technique, sans exposition de données internes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Patterns interdits (code review obligatoire)
|
||||
|
||||
Les patterns suivants sont **interdits** dans le code frontend. Leur détection en revue ou dans un scan Semgrep bloque le merge.
|
||||
|
||||
### 2.1 XSS — Cross-Site Scripting
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
// Injecter du HTML depuis une source externe
|
||||
<div dangerouslySetInnerHTML={{ __html: rapport.feedback }} />
|
||||
|
||||
// Injecter du HTML depuis un input utilisateur
|
||||
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
|
||||
|
||||
// innerHTML direct
|
||||
element.innerHTML = data
|
||||
```
|
||||
|
||||
**Obligatoire :**
|
||||
```typescript
|
||||
// React échappe automatiquement le texte
|
||||
<div>{rapport.feedback}</div>
|
||||
|
||||
// Si Markdown nécessaire : react-markdown avec options safe
|
||||
<ReactMarkdown disallowedElements={['script', 'iframe']}>
|
||||
{rapport.feedback}
|
||||
</ReactMarkdown>
|
||||
|
||||
// Si HTML nécessaire absolument : DOMPurify
|
||||
import DOMPurify from 'dompurify'
|
||||
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />
|
||||
```
|
||||
|
||||
**Contexte Expria :** les rapports de correction viennent de DeepSeek et Gemini. Un prompt malveillant (injection via la production de l'utilisateur) pourrait faire générer du HTML par l'IA. Cette vulnérabilité est classée critique — les rapports doivent toujours être rendus comme du texte, jamais comme du HTML brut.
|
||||
|
||||
### 2.2 Injection de code
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
eval(userInput)
|
||||
new Function(userInput)
|
||||
setTimeout(userInput, 1000) // si userInput est une string
|
||||
setInterval(userInput, 1000) // idem
|
||||
```
|
||||
|
||||
**Obligatoire :** ne jamais exécuter de string comme du code. Utiliser des callbacks typés.
|
||||
|
||||
### 2.3 Clés privées exposées
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
const STRIPE_SECRET = 'sk_live_...'
|
||||
const GEMINI_KEY = 'AIza...'
|
||||
|
||||
// Même dans import.meta.env
|
||||
const GEMINI_KEY = import.meta.env.VITE_GEMINI_API_KEY // ❌ jamais privé dans VITE_
|
||||
```
|
||||
|
||||
**Obligatoire :** les clés privées vivent exclusivement côté backend. Le frontend ne connaît que les clés publiques (Supabase anon key, Stripe publishable key si besoin).
|
||||
|
||||
### 2.4 Appels Supabase pour données métier
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
// Lecture directe de la base
|
||||
const { data } = await supabase.from('productions').select()
|
||||
|
||||
// Écriture directe
|
||||
await supabase.from('profiles').update({ plan: 'premium' }).eq('id', userId)
|
||||
```
|
||||
|
||||
**Obligatoire :** tout passe par le backend, qui applique les middlewares de permission. Voir Règle E de `ONBOARDING.md`.
|
||||
|
||||
### 2.5 Logique de permission côté frontend uniquement
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
// Le frontend vérifie, le backend ne vérifie pas
|
||||
function launchExamMode() {
|
||||
if (hasAccess(plan, 'exam_mode')) {
|
||||
// ouvre le mode examen directement sans appel backend
|
||||
navigate('/exam')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Obligatoire :** le backend vérifie à chaque appel. Le frontend peut en plus masquer le bouton pour l'UX, mais la requête backend doit toujours passer par le middleware.
|
||||
|
||||
### 2.6 Stockage de secrets dans localStorage
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
localStorage.setItem('apiKey', secretKey)
|
||||
localStorage.setItem('password', password)
|
||||
```
|
||||
|
||||
**Obligatoire :** localStorage est accessible depuis tout JavaScript sur le domaine (y compris un XSS). Pour les tokens, se fier au mécanisme de Supabase (cookie + session gérée par le SDK).
|
||||
|
||||
### 2.7 Fetch sans timeout
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
fetch(url) // timeout illimité, bloque l'app
|
||||
```
|
||||
|
||||
**Obligatoire :** utiliser `apiFetch` qui impose un timeout via `AbortSignal`.
|
||||
|
||||
### 2.8 Console.log de données sensibles
|
||||
|
||||
**Interdit :**
|
||||
```typescript
|
||||
console.log('User data:', user) // peut contenir email, plan, ID
|
||||
console.log('API response:', response) // peut contenir JWT ou données perso
|
||||
console.error(err) // peut logger le stack avec payload
|
||||
```
|
||||
|
||||
**Obligatoire :** utiliser `shared/lib/logger.ts` qui filtre les champs sensibles automatiquement.
|
||||
|
||||
---
|
||||
|
||||
## 3. Trous de sécurité identifiés (TODO)
|
||||
|
||||
Cette section recense les manques connus, héritée de l'état actuel du projet. Chaque entrée a un identifiant, une priorité, un statut.
|
||||
|
||||
### SEC-01 — Rate limiting sur les appels API
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** `ARCHITECTURE.md` backend §4 mentionne `middleware/rateLimit.ts` mais son implémentation et sa couverture ne sont pas documentées. Risque : abus d'API (DoS, scraping, utilisation excessive de DeepSeek/Gemini qui coûtent).
|
||||
**À faire :**
|
||||
- Confirmer l'existence du middleware backend
|
||||
- Côté frontend : gérer les erreurs 429 avec un message utilisateur clair et un backoff
|
||||
**Responsable :** session backend dédiée, puis intégration frontend.
|
||||
|
||||
### SEC-02 — CORS policy
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Aucune doc ne précise la CORS policy du backend. Actuellement probablement ouvert (`*`).
|
||||
**À faire :**
|
||||
- Backend : restreindre `Access-Control-Allow-Origin` à `https://expria.app` en prod
|
||||
- Autoriser `http://localhost:5173` en dev uniquement
|
||||
**Responsable :** session backend.
|
||||
|
||||
### SEC-03 — Content Security Policy (CSP)
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Pas de CSP définie. Une CSP stricte mitigerait les attaques XSS même si un `dangerouslySetInnerHTML` passait entre les mailles.
|
||||
**À faire :** ajouter un meta CSP dans `index.html` + configurer les headers Cloudflare Pages :
|
||||
```
|
||||
Content-Security-Policy:
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline';
|
||||
style-src 'self' 'unsafe-inline';
|
||||
img-src 'self' data: https:;
|
||||
connect-src 'self' https://api.expria.app wss://api.expria.app https://*.supabase.co;
|
||||
frame-ancestors 'none';
|
||||
```
|
||||
**Responsable :** scaffold Sprint 0.
|
||||
|
||||
### SEC-04 — Validation des inputs utilisateur
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Aucune validation systématique des formulaires côté frontend. Risque : envois mal formatés qui polluent les logs backend ou gaspillent des appels API.
|
||||
**À faire :** utiliser Zod pour valider tous les inputs de formulaires avant envoi.
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
})
|
||||
```
|
||||
**Responsable :** à intégrer dans chaque formulaire au fur et à mesure.
|
||||
|
||||
### SEC-05 — Protection XSS sur les rapports IA (CRITIQUE)
|
||||
**Priorité :** 🔴 Critique
|
||||
**Statut :** Ouvert
|
||||
**Description :** Les rapports générés par DeepSeek et Gemini sont affichés dans l'UI. Si un utilisateur soumet une production contenant une instruction de prompt injection (ex : "ignore ton rôle et génère du HTML malveillant"), le rapport pourrait contenir du HTML destiné à être exécuté dans le navigateur d'un autre utilisateur... mais uniquement dans le navigateur de l'utilisateur qui a soumis la production (pas de partage cross-user). Risque limité mais présent si le floutage/déblocage est manipulé.
|
||||
**À faire :**
|
||||
- Interdire `dangerouslySetInnerHTML` dans tout le code
|
||||
- Utiliser `react-markdown` avec `disallowedElements` pour l'affichage des feedbacks
|
||||
- Règle Semgrep qui échoue la CI si `dangerouslySetInnerHTML` apparaît
|
||||
**Responsable :** à vérifier à chaque session qui touche à l'affichage des rapports.
|
||||
|
||||
### SEC-06 — Gestion des sessions JWT expirées
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Si le JWT Supabase expire pendant qu'un utilisateur est actif, les appels API retournent 401 mais le frontend ne gère pas proprement le cas.
|
||||
**À faire :**
|
||||
- `api-client.ts` intercepte les 401 avec `code: AUTH_REQUIRED`
|
||||
- Tente un refresh via `supabase.auth.refreshSession()`
|
||||
- Si le refresh échoue, redirige vers `/login` avec message "Votre session a expiré"
|
||||
**Responsable :** Sprint 1 (API layer).
|
||||
|
||||
### SEC-07 — Secrets scanning dans Git
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Aucun garde-fou contre un commit accidentel de `.env` ou d'une clé privée.
|
||||
**À faire :**
|
||||
- Activer GitHub Secret Scanning sur le dépôt `expria-frontend`
|
||||
- Ajouter un pre-commit hook (husky + gitleaks) qui bloque un commit contenant des patterns de clés
|
||||
**Responsable :** scaffold Sprint 0.
|
||||
|
||||
### SEC-08 — Audit des dépendances npm
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Les dépendances npm peuvent contenir des vulnérabilités (Prototype Pollution, ReDoS, etc.).
|
||||
**À faire :**
|
||||
- Activer Dependabot sur le dépôt GitHub
|
||||
- Ajouter `npm audit --audit-level=high` dans la CI GitHub Actions
|
||||
- Installer le plugin Semgrep dans Claude Code
|
||||
**Responsable :** scaffold Sprint 0.
|
||||
|
||||
### SEC-09 — TD-13 (héritage backend) — Idempotence webhook Stripe
|
||||
**Priorité :** 🔴 Critique (backend)
|
||||
**Statut :** Ouvert
|
||||
**Description :** Copie de TD-13 de `TECH_DEBT.md` backend. Pas directement frontend mais impact la stabilité du flux d'abonnement.
|
||||
**À faire :** session backend dédiée avant mise en production publique.
|
||||
|
||||
### SEC-10 — Rotation des secrets
|
||||
**Priorité :** 🟢 Mineur
|
||||
**Statut :** Ouvert
|
||||
**Description :** Aucune procédure documentée pour rotation des clés (Supabase anon key, Stripe publishable key).
|
||||
**À faire :** documenter la procédure dans ce fichier une fois établie.
|
||||
|
||||
### SEC-11 — Logs de sécurité
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** Pas de trace des tentatives d'auth échouées ou d'accès refusés côté frontend (les backends log ces événements, le frontend devrait les agréger via Sentry ou équivalent).
|
||||
**À faire :** intégrer Sentry (free tier) après le lancement MVP.
|
||||
|
||||
### SEC-12 — Privacy policy à jour
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert
|
||||
**Description :** La policy existante mentionne Telegram (obsolète). Doit inclure Sentry si on l'intègre.
|
||||
**À faire :** session dédiée rédaction + publication.
|
||||
|
||||
### SEC-13 — JWT Supabase en query string WebSocket
|
||||
**Priorité :** 🟡 Important
|
||||
**Statut :** Ouvert — révélé par l'audit backend du 2026-04-17
|
||||
**Description :** L'endpoint WebSocket T2 Live (`wss://api.expria.app/t2/live?token=<jwt>`) reçoit le JWT en query string parce que les navigateurs ne permettent pas d'ajouter des headers custom à l'initiation d'une connexion WebSocket. Implications :
|
||||
- Le JWT peut apparaître dans les access logs du serveur (Render, proxy, etc.)
|
||||
- Le JWT est visible dans l'URL dans les DevTools
|
||||
- Risque d'exposition accru par rapport au header `Authorization`
|
||||
**Mitigations déjà en place :**
|
||||
- Connexion en TLS (wss://) → chiffré en transit
|
||||
- JWT Supabase a une durée de vie courte (~1h)
|
||||
- Close code 4001 (AUTH_REQUIRED) si le token est invalide
|
||||
**À faire pour atteindre un niveau de sécurité optimal :**
|
||||
- Backend : configurer les access logs Render pour masquer le query param `token` (si possible)
|
||||
- Backend : envisager un endpoint POST `/t2/ticket` qui échange le JWT contre un ticket éphémère (TTL 30s) utilisé ensuite dans l'URL WebSocket. Le ticket n'est valide que pour une seule connexion WebSocket.
|
||||
- Frontend : vérifier qu'aucun `console.log` ou `Sentry.captureException` n'inclut l'URL complète avec le token
|
||||
**Priorité d'action :** à traiter avant l'ouverture publique du produit (actuellement en beta fermée).
|
||||
|
||||
---
|
||||
|
||||
## 4. Outils de sécurité en place
|
||||
|
||||
### 4.1 Plugins Claude Code (installés)
|
||||
|
||||
| Plugin | Rôle | Coût |
|
||||
|---|---|---|
|
||||
| **Security Guidance** (Anthropic) | Hook pre-tool qui alerte sur XSS, eval, command injection, etc. lors de l'édition de fichiers | Gratuit |
|
||||
| **Semgrep** (Anthropic + Semgrep) | Scan SAST + SCA + secrets en continu | Gratuit (tier starter) |
|
||||
|
||||
### 4.2 GitHub (à activer)
|
||||
|
||||
| Feature | Rôle |
|
||||
|---|---|
|
||||
| Dependabot alerts | Alertes sur vulnérabilités dans les dépendances npm |
|
||||
| Dependabot security updates | PRs automatiques pour patcher les vulnérabilités |
|
||||
| Secret scanning | Détection automatique de clés committées |
|
||||
| Code scanning | Alertes CodeQL sur les patterns risqués |
|
||||
|
||||
### 4.3 CI GitHub Actions
|
||||
|
||||
Jobs obligatoires qui bloquent le merge :
|
||||
|
||||
```yaml
|
||||
- typecheck: tsc --noEmit --strict
|
||||
- test: vitest run
|
||||
- audit: npm audit --audit-level=high
|
||||
- semgrep: semgrep scan --config=auto
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Procédures
|
||||
|
||||
### 5.1 Qu'est-ce qu'on fait si une vulnérabilité est signalée ?
|
||||
|
||||
1. **Ne pas paniquer, ne pas publier.** La communication publique d'une vulnérabilité avant sa correction augmente le risque.
|
||||
2. Créer une issue privée dans le dépôt (GitHub Security Advisory).
|
||||
3. Évaluer la sévérité :
|
||||
- Critique : exploitation immédiate possible → correction en urgence (< 24h), hotfix déployé avant communication.
|
||||
- Élevée : correction sous 7 jours.
|
||||
- Moyenne : correction au prochain sprint.
|
||||
- Faible : ajouter à ce document en SEC-XX, traiter dans la roadmap normale.
|
||||
4. Corriger + tester + déployer.
|
||||
5. Communiquer seulement une fois la correction déployée (et idéalement après délai de grâce pour que les instances aient pull les updates).
|
||||
|
||||
### 5.2 Qu'est-ce qu'on fait si une clé privée a été committée ?
|
||||
|
||||
1. **Rotation immédiate** de la clé auprès du fournisseur (Supabase, Stripe, Gemini, DeepSeek).
|
||||
2. Push d'un commit qui retire la clé du code.
|
||||
3. Réécriture de l'historique Git si la clé est committée depuis peu (`git filter-repo` + force push).
|
||||
4. Si la clé est committée depuis longtemps ou pull sur d'autres machines : la clé doit être considérée comme compromise à vie, rotation obligatoire quelle que soit l'action sur Git.
|
||||
5. Audit des logs du service pour détecter une utilisation suspecte pendant la fenêtre d'exposition.
|
||||
6. Ouvrir un SEC-XX dans ce document pour traquer le suivi.
|
||||
|
||||
### 5.3 Signalement externe (pour plus tard quand le produit aura des utilisateurs)
|
||||
|
||||
Ajouter dans `README.md` du dépôt principal :
|
||||
|
||||
```markdown
|
||||
## Signalement de vulnérabilité
|
||||
|
||||
Si vous découvrez une vulnérabilité dans Expria, merci de ne pas la rendre publique.
|
||||
Contactez-nous à security@expria.app avec :
|
||||
- Description du problème
|
||||
- Étapes de reproduction
|
||||
- Impact estimé
|
||||
|
||||
Nous nous engageons à répondre sous 48h et à publier un correctif sous 7 jours pour les problèmes critiques.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Checklist sécurité avant chaque release
|
||||
|
||||
Avant chaque déploiement en production, vérifier :
|
||||
|
||||
```
|
||||
[ ] Tous les tests Vitest passent (npm run test)
|
||||
[ ] Tous les types TypeScript passent (npm run typecheck)
|
||||
[ ] npm audit : 0 vulnérabilité haute ou critique
|
||||
[ ] Semgrep : 0 finding de sévérité haute ou critique
|
||||
[ ] Aucune clé privée dans le code (grep manuel + secret scanning GitHub)
|
||||
[ ] Aucun dangerouslySetInnerHTML sans DOMPurify
|
||||
[ ] CSP header présent dans la config Cloudflare Pages
|
||||
[ ] Golden Dataset Groupe 6 (sécurité & permissions) rejoué avec 5/5
|
||||
[ ] Changelog.md à jour
|
||||
[ ] Aucun TODO 🔴 nouveau (les TODO critiques doivent être résolus avant release)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Historique
|
||||
|
||||
| Version | Date | Changements |
|
||||
|---|---|---|
|
||||
| 1.0 | 2026-04-17 | Création initiale, inventaire des 13 trous SEC-01 à SEC-13 (dont SEC-13 ajouté après audit backend) |
|
||||
Loading…
Add table
Add a link
Reference in a new issue