45edc1fa77
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>
197 lines
8.4 KiB
Markdown
197 lines
8.4 KiB
Markdown
# 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.
|