Licence EUPL-1.2 + hardening anti-abus

P1 — Licence :
- Ajout du fichier LICENSE (EUPL-1.2 complet)
- README mis à jour : section licence, table docs, vars d'environnement
- En-têtes EUPL ajoutés dans les fichiers sources principaux (Flask, React)

P2 — Hardening anti-abus :
- Rate limiting Redis-ready (REDIS_URL) avec clé fingerprint + IP
- Honeypot anti-bot : champ caché côté client + vérification serveur
- Fingerprinting non-PII via FingerprintJS (hash SHA-256, colonne ideas.fingerprint_hash)
- Cooldown session : cookie httpOnly signé HMAC-SHA256 (SECRET_KEY requis)
- Détection de flood : alerte WARNING si > FLOOD_THRESHOLD soumissions / 5 min
- hCaptcha stub : intégré, activable via HCAPTCHA_SECRET_KEY + VITE_HCAPTCHA_SITE_KEY
- Nouvelles dépendances : redis (backend), @fingerprintjs/fingerprintjs + @hcaptcha/react-hcaptcha (frontend)
- docs/SECURITE_ANTI_ABUS.md : documentation complète des seuils et de la configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 18:05:46 +02:00
parent 57211ad393
commit 45edc1fa77
14 changed files with 881 additions and 38 deletions
+196
View File
@@ -0,0 +1,196 @@
# Sécurité anti-abus — La Voix du Peuple
> Document technique décrivant les protections contre les attaques sybil, les floods de bots et les brigading coordonnés. Mis à jour : mai 2026.
---
## Contexte et risque
La plateforme est conçue sans authentification (choix philosophique préservant l'anonymat). Cette absence rend triviale, sans protection, la manipulation des données par :
- **Sybil attacks** : multiplication des soumissions depuis la même entité
- **Bot floods** : soumissions automatisées en masse
- **Brigading coordonné** : campagnes organisées pour inonder la synthèse de contenus orientés
Les protections suivantes traitent ce risque sans remettre en cause l'anonymat des contributeurs légitimes.
---
## Couches de protection (ordre d'application)
### 1. Honeypot anti-bot
**Principe** : un champ de formulaire est présent dans le HTML mais rendu invisible aux utilisateurs réels (`display: none`, `position: absolute`, `aria-hidden="true"`). Les bots qui analysent le DOM et remplissent tous les champs déclenchent le honeypot.
**Comportement** :
- **Client** : si le champ `_hp` a une valeur lors de la soumission, l'appel API n'est pas effectué. Réponse simulée silencieuse côté JS.
- **Serveur** : si `_hp` est présent et non vide dans le corps JSON, le serveur retourne un `201` factice sans enregistrer quoi que ce soit (`logger.info("Honeypot déclenché")`).
**Fichiers** : `artifacts/voix-du-peuple/src/pages/home.tsx` · `artifacts/flask-api/app.py`
---
### 2. hCaptcha (stub — activer en production)
**Principe** : widget CAPTCHA humain présenté avant la soumission. hCaptcha est choisi pour :
- Gratuit (tier communautaire)
- RGPD-compliant (pas de cookies tiers, données UE)
- Pas de dépendance à Google
**État actuel** : stub intégré, désactivé par défaut.
**Pour activer** :
1. Créer un compte sur [hcaptcha.com](https://www.hcaptcha.com/)
2. Créer un site, récupérer la clé de site et la clé secrète
3. Configurer les variables d'environnement :
```env
# Frontend (.env dans artifacts/voix-du-peuple/)
VITE_HCAPTCHA_SITE_KEY=votre-cle-de-site
# Backend (.env ou variable système)
HCAPTCHA_SECRET_KEY=votre-cle-secrete
```
4. Reconstruire le frontend : `pnpm build`
**Comportement quand activé** :
- Le widget hCaptcha s'affiche dans le formulaire avant le bouton "Contribuer"
- Le bouton est désactivé tant que le CAPTCHA n'est pas validé
- Le token est transmis dans l'en-tête `X-HCaptcha-Token`
- Le backend vérifie le token via l'API hCaptcha (`https://hcaptcha.com/siteverify`)
- Si la clé secrète n'est pas configurée côté serveur, la vérification est sautée (dégradation gracieuse)
**Fichiers** : `artifacts/voix-du-peuple/src/pages/home.tsx` · `artifacts/flask-api/app.py` (`_verify_hcaptcha()`)
---
### 3. Rate limiting par IP + fingerprint
**Outil** : Flask-Limiter v3 avec stockage Redis (si `REDIS_URL` défini) ou mémoire (dev).
**Seuils par défaut** (configurables via variables d'environnement) :
| Endpoint | Limite par défaut | Variable de contrôle |
|----------|------------------|----------------------|
| `POST /api/ideas` | 5/min · **3/heure** | `RATE_LIMIT_CONTRIBUTIONS` |
| `POST /api/ideas/:id/flag` | 3/min · 10/heure | — |
| `POST /api/admin/login` | 10/min | — |
| Toutes routes | 60/heure · 200/jour | — |
**Clé de rate limiting** : priorité au fingerprint FingerprintJS (hashé), sinon IP. Empêche de contourner la limite en changeant d'IP si le fingerprint est reconnu.
**Pour activer Redis** :
```env
REDIS_URL=redis://localhost:6379/0
```
Sans Redis, le rate limiting est en mémoire (reset au redémarrage — suffisant pour une instance unique).
**Fichiers** : `artifacts/flask-api/app.py` (`get_fingerprint_key()`, `RATE_LIMIT_CONTRIBUTIONS`)
---
### 4. Fingerprinting non-PII
**Outil** : `@fingerprintjs/fingerprintjs` v4 (open source, pas de compte requis).
**Principe** : FingerprintJS génère un `visitorId` côté client à partir de caractéristiques du navigateur (User-Agent, timezone, canvas fingerprint, etc.) sans créer de cookie tiers ni stocker de données personnelles.
**Flux** :
1. À l'initialisation de l'app React (`App.tsx`), FingerprintJS est chargé
2. Le `visitorId` est stocké en mémoire (non persisté)
3. Il est envoyé sur chaque requête API dans l'en-tête `X-Visitor-Id`
4. Le backend le hash en SHA-256 (32 premiers hex) avant tout stockage
5. Le hash est enregistré en base (`ideas.fingerprint_hash`) pour analyse post-hoc si nécessaire
**Données stockées** : uniquement le hash SHA-256 tronqué — non-réversible, non-PII au sens du RGPD. Aucun cookie, aucun suivi cross-site.
**Fichiers** : `artifacts/voix-du-peuple/src/App.tsx` · `lib/api-client-react/src/custom-fetch.ts` · `artifacts/flask-api/app.py` · `artifacts/flask-api/database.py`
---
### 5. Cooldown par session (cookie httpOnly signé)
**Principe** : après une soumission acceptée, un cookie httpOnly signé HMAC-SHA256 est posé. Toute tentative de soumission avant l'expiration du cooldown est rejetée avec un `429`.
**Durée par défaut** : 3600 secondes (1 heure), configurable :
```env
CONTRIBUTION_COOLDOWN_SECONDS=3600
```
**Signature** : le cookie `_cv` contient `{timestamp}.{signature}` où la signature est `HMAC-SHA256(SECRET_KEY, timestamp_bytes)[:16]`. Impossible de forger sans connaître `SECRET_KEY`.
**Prérequis** :
```env
SECRET_KEY=une-longue-chaine-aleatoire-minimum-32-chars
```
Si `SECRET_KEY` n'est pas défini, le cooldown est désactivé (dégradation gracieuse).
**Limite** : fonctionne pleinement en production (même domaine, Nginx reverse proxy). En développement cross-origin (Vite sur port différent de Flask), le cookie n'est pas envoyé automatiquement par le navigateur (CORS `supports_credentials=False`).
**Fichiers** : `artifacts/flask-api/app.py` (`_sign_cooldown()`, `_verify_cooldown()`)
---
### 6. Détection de flood
**Principe** : compteur en mémoire par IP (et par fingerprint si disponible) sur une fenêtre glissante de 5 minutes. Si le seuil est dépassé, une alerte `WARNING` est émise dans les logs.
**Seuil par défaut** : 10 soumissions en 5 minutes, configurable :
```env
FLOOD_THRESHOLD=10
```
**Ce qui se passe** : l'alerte est loggée mais la soumission n'est pas bloquée (le rate limiter Flask-Limiter s'en charge). L'objectif est d'alerter l'opérateur pour investigation.
**Format de l'alerte** :
```
WARNING ALERTE FLOOD — IP: 1.2.3.4 | fingerprint: abc123... | seuil: 10/5min
```
**Pour aller plus loin** : brancher sur un webhook (email Mailgun/Brevo, Slack/Mattermost) via un hook sur les logs `WARNING` avec le pattern `ALERTE FLOOD`.
**Limite** : l'état est en mémoire et se réinitialise au redémarrage. Pour une persistance cross-restart, utiliser Redis directement avec `EXPIRE`.
**Fichiers** : `artifacts/flask-api/app.py` (`_check_flood()`, `_flood_tracker`)
---
## Variables d'environnement récapitulatif
```env
# Rate limiting
REDIS_URL=redis://localhost:6379/0 # Optionnel — sinon mémoire
RATE_LIMIT_CONTRIBUTIONS=5 per minute;3 per hour # Format flask-limiter
# Cooldown session
SECRET_KEY=une-longue-chaine-aleatoire-minimum-32-chars
CONTRIBUTION_COOLDOWN_SECONDS=3600
# Flood detection
FLOOD_THRESHOLD=10
# hCaptcha (désactivé si absent)
HCAPTCHA_SECRET_KEY=votre-cle-secrete # Backend
VITE_HCAPTCHA_SITE_KEY=votre-cle-de-site # Frontend (nécessite rebuild)
```
---
## Ce que ces protections ne couvrent pas
- **Bots sophistiqués JavaScript-capable** : FingerprintJS peut être contourné par un navigateur headless bien configuré. La combinaison IP + fingerprint + hCaptcha rend l'attaque coûteuse mais pas impossible.
- **VPN / Tor** : le rate limiting IP peut être contourné. Le fingerprint compense partiellement.
- **Submissions manuelles coordonnées** (brigading humain) : seul le contenu + la modération IA protège contre ce vecteur.
- **Cross-origin en dev** : le cookie cooldown ne fonctionne pas en développement (ports différents, CORS sans credentials).
---
## Évolutions futures recommandées
1. **Redis** : déployer Redis et configurer `REDIS_URL` en production pour un rate limiting persistant et cross-process.
2. **hCaptcha** : activer dès que la plateforme est ouverte au public (clé gratuite, 5 minutes de setup).
3. **Alerte flood automatique** : brancher un webhook Brevo/Mailgun sur les logs `ALERTE FLOOD`.
4. **CAPTCHA invisible** : envisager hCaptcha en mode "invisible" (score-based) pour ne pas imposer de défi aux utilisateurs légitimes.