Persistence: PostgreSQL table ip_abuse (ip_hash SHA-256, rejection_count, timestamps, last_idea_id, blacklisted_at, expires_at). No raw IP stored. Logic: - First illegal contribution: recorded, tolerated (benefit of the doubt) - Second illegal contribution: 30-day block (IP_BLACKLIST_DAYS, configurable) - Counter is cumulative — valid contributions do not reset it - Block check fires before all other validations in both submit routes Backend: - database.py: check_ip_blacklist, record_ip_rejection, get_ip_blacklist, remove_ip_blacklist; ip_abuse table created in init_db() - app.py: _get_ip_hash() helper; blacklist check + record in submit_idea() and submit_consultation_idea(); admin routes GET/DELETE /api/admin/ip-blacklist Admin panel: new "Blacklist" tab showing active entries with hash, rejection count, trigger idea id, dates; "Lever" button for manual removal Docs: - SECURITE_ANTI_ABUS.md: section 8 describing the mechanism, RGPD basis, limits, and IP_BLACKLIST_DAYS env var - privacy-policy.tsx: IP row updated to reflect hash storage + legal basis Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
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
_hpa une valeur lors de la soumission, l'appel API n'est pas effectué. Réponse simulée silencieuse côté JS. - Serveur : si
_hpest présent et non vide dans le corps JSON, le serveur retourne un201factice 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 :
- Créer un compte sur hcaptcha.com
- Créer un site, récupérer la clé de site et la clé secrète
- Configurer les variables d'environnement :
# 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
- 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 :
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 :
- À l'initialisation de l'app React (
App.tsx), FingerprintJS est chargé - Le
visitorIdest stocké en mémoire (non persisté) - Il est envoyé sur chaque requête API dans l'en-tête
X-Visitor-Id - Le backend le hash en SHA-256 (32 premiers hex) avant tout stockage
- 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 :
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 :
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 :
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)
8. Blacklist IP automatique
Principe : lorsqu'une IP soumet deux contributions illégales (rejetées par le filtre IA), elle est automatiquement bloquée pour une durée configurable. La première soumission illégale est tolérée (bénéfice du doute) ; la deuxième déclenche le blocage, quelle que soit la durée écoulée entre les deux.
Caractéristiques RGPD :
- L'adresse IP brute n'est jamais stockée. Seul un hash SHA-256 tronqué (32 caractères, non réversible) est conservé.
- Base légale : intérêt légitime (RGPD art. 6(1)(f)) — protection de la plateforme contre les contenus illicites.
- Durée de conservation : 30 jours par défaut (
IP_BLACKLIST_DAYS), configurable. - Mention dans la politique de confidentialité : oui (section « Quelles données sont collectées ? »).
Implémentation :
- Table
ip_abuse(PostgreSQL) :ip_hash,rejection_count, horodatages,last_idea_id,blacklisted_at,expires_at. - Au 2ème rejet :
blacklisted_atetexpires_atsont renseignés ; toute tentative ultérieure retourne403 ip_blacklisted. - Le compteur est cumulatif : une contribution valide entre deux rejets ne remet pas le compteur à zéro.
- Persistant cross-workers et cross-restarts (PostgreSQL, pas mémoire).
Gestion admin :
- Le panel admin affiche les IPs blacklistées actives (hash + date + contribution déclencheuse).
- Un bouton « Lever » permet à l'admin de supprimer une entrée manuellement (faux positif, recours).
- Route :
GET /api/admin/ip-blacklist·DELETE /api/admin/ip-blacklist/<ip_hash>
Limite : contournable par changement d'IP (VPN, Tor). Combiné avec le fingerprint FingerprintJS, le contournement est plus coûteux mais reste possible.
Fichiers : artifacts/flask-api/database.py (check_ip_blacklist, record_ip_rejection, get_ip_blacklist, remove_ip_blacklist) · artifacts/flask-api/app.py (_get_ip_hash, routes admin)
Variables d'environnement récapitulatif
# 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)
# Blacklist IP
IP_BLACKLIST_DAYS=30 # Durée du blocage après 2 rejets illégaux
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
- Redis : déployer Redis et configurer
REDIS_URLen production pour un rate limiting persistant et cross-process. - hCaptcha : activer dès que la plateforme est ouverte au public (clé gratuite, 5 minutes de setup).
- Alerte flood automatique : brancher un webhook Brevo/Mailgun sur les logs
ALERTE FLOOD. - CAPTCHA invisible : envisager hCaptcha en mode "invisible" (score-based) pour ne pas imposer de défi aux utilisateurs légitimes.