Pare-feu applicatif (WAF) pour SPIP. Bloque les requêtes malveillantes et bannit les récidivistes.
Installation
SPIP WAF s’installe comme n’importe quel plugin SPIP.
Contexte
SPIP est un CMS sécurisé, doté d’un solide historique en matière de sécurité et de stabilité. Parallèlement, les webmasters doivent faire face à un flot croissant d’attaques visant leurs serveurs. Ce plugin bloque les requêtes malveillantes et contribue à protéger les sites contre le trafic indésirable.
Fonctionnement
Le WAF a deux volets.
1. BLOCAGE PAR RÈGLES :
- Des règles vérifient chaque requête à la recherche de motifs suspects (injection SQL, traversée de répertoire, sondes CMS, etc.)
- Chaque violation est bloquée et enregistrée. L’utilisateur voit un écran de blocage avec un bouton pour se débloquer et revenir à la page d’accueil (utile en cas de faux positif).
- Après plusieurs violations, l’IP est bannie.
- Les visiteurs bannis voient un formulaire de déblocage (utile en cas de faux positif). Via le bouton, ils peuvent se débloquer et revenir à la page d’accueil.
- S’ils continuent à déclencher des règles après le déblocage, ils sont bannis à nouveau.
- Après trop de bannissements, l’IP est bloquée définitivement (hard-block). Dans ce cas, les utilisateurs (bots) reçoivent une simple erreur 403.
Flux : Bloqué → Banni → Bloqué définitivement
2. BLOCAGE PAR LISTES PUBLIQUES D’IP :
Le plugin s’intègre aussi avec des listes publiques d’IP malveillantes, comme FireHOL et Spamhaus, que l’on peut activer dans le formulaire de configuration. Une fois activées, le WAF bloque toutes les IP figurant sur ces listes, car elles sont déjà identifiées comme des IP malveillantes connues.
Flux : IP détectée sur une liste activée → Bloquée définitivement
Fonctionnalités
Tableau de bord
Le tableau de bord du WAF dans l’espace privé de SPIP (Activité › SPIP WAF) donne un aperçu de l’activité récente. Il affiche les requêtes bloquées, les IP bannies et les IP bloquées définitivement, ainsi que les règles et listes qui les ont déclenchées. Il permet de surveiller le trafic, de repérer les faux positifs et de vérifier quelles IP ont été bloquées ou bannies.
Capture d’écran : Début du tableau de bord

Capture d’écran : Vue détaillée du tableau de bord

Configuration
Le formulaire de configuration (Configuration › SPIP WAF) permet d’activer le WAF et d’ajuster son comportement.
Capture d’écran : Formulaire de configuration, Entête IP

Capture d’écran : Formulaire de configuration, Listes

Il faut d’abord définir l’en-tête IP du client pour que le WAF commence à fonctionner.
On peut aussi ajuster les seuils d’avertissements et de blocage, la liste autorisée et la liste de refus, et sélectionner les listes publiques d’IP à appliquer. Voir la section Configuration ci-dessous pour la liste complète des réglages disponibles.
Bloqué par erreur ?
Si vous avez été bloqué définitivement (par exemple après des tests avec des charges malveillantes), ajoutez votre IP en liste autorisée via mes_options.php.
Trouver votre IP sur https://iplocation.net (ça ressemble à 203.0.113.42).
Astuce : Si vous ne pouvez plus du tout accéder au site, passez sur un autre réseau (par exemple un partage de connexion depuis votre téléphone), connectez-vous à /ecrire depuis là, et ajoutez votre IP dans le formulaire de configuration du WAF. Puis revenez sur votre réseau habituel.
Pour ajouter une IP en liste autorisée via le code, ajoutez ceci dans config/mes_options.php (créez le fichier si nécessaire) :
define('_WAF_ALLOWLIST', '203.0.113.42');
Plusieurs IP, une par ligne :
define('_WAF_ALLOWLIST',
'203.0.113.42
198.51.100.7
198.51.100.8'
);
Une fois le fichier enregistré, le WAF laisse passer ces IP.
AVANCÉ
Tout ce qui suit concerne la configuration avancée, utile uniquement dans des cas particuliers.
Configuration
La plupart des réglages sont disponibles dans le formulaire de configuration du WAF dans l’espace privé de SPIP (Configuration › SPIP WAF). Ils sont stockés dans la table meta de SPIP et peuvent être surchargés en définissant la constante _WAF_* correspondante dans config/mes_options.php avant le chargement du plugin.
Réglages du formulaire de configuration
| Réglage | Constante de surcharge | Défaut | Description |
|---|---|---|---|
| En-tête IP du client | _WAF_CLIENT_IP_HEADER |
(aucun) | En-tête HTTP contenant la vraie IP du client (par ex. HTTP_X_FORWARDED_FOR). Doit être défini pour que le WAF s’active. |
| Proxies de confiance | _WAF_TRUSTED_PROXIES |
(vide) | IP/CIDR des proxies de confiance, un par ligne. Utilisé pour parcourir la chaîne X-Forwarded-For de droite à gauche. |
| Seuil d’avertissements | _WAF_STRIKE_THRESHOLD |
3 |
Nombre de violations avant qu’une IP soit bannie. |
| Seuil de blocage | _WAF_BLOCK_THRESHOLD |
10 |
Nombre de bannissements avant qu’une IP soit bloquée définitivement (pas de formulaire de déblocage). |
| Longueur max d’un paramètre GET | _WAF_MAX_GET_PARAM_LENGTH |
2000 |
Longueur maximale autorisée pour la valeur d’un paramètre GET. |
| Durée de validité du jeton de déblocage | _WAF_UNBLOCK_TOKEN_TTL |
900 |
Durée de validité du jeton de déblocage en libre-service, en secondes. |
| Afficher le formulaire de déblocage | _WAF_UNBLOCK_FORM |
true |
Si les IP bannies voient un formulaire de déblocage en libre-service ou une simple erreur 403. |
| Liste autorisée | _WAF_ALLOWLIST |
(vide) | IP/CIDR qui contournent tous les contrôles du WAF, un par ligne. |
| Liste de refus | _WAF_DENYLIST |
(vide) | IP/CIDR toujours bloqués, un par ligne. |
| Avertissement sur échec de connexion | _WAF_LOGIN_STRIKE |
false |
Compter les tentatives de connexion échouées comme des avertissements WAF. |
| Durée du cache | _WAF_CACHE_TTL |
86400 |
Durée du cache WAF en secondes (compteurs d’avertissements, drapeaux de bannissement, etc.). |
| Listes activées | _WAF_ENABLED_BLOCKLISTS |
firehol_level1,firehol_level2 |
Identifiants des listes publiques d’IP à appliquer, séparés par des virgules (gérés via les cases à cocher du formulaire). |
Configuration avancée
Les constantes suivantes ne peuvent être définies que dans config/mes_options.php. Elles ne sont pas exposées dans le formulaire de configuration.
Constantes uniquement par code
| Constante | Défaut | Description |
|---|---|---|
_WAF_TARPIT_DELAY |
0 |
Délai en secondes avant d’envoyer la réponse 403 (0 = désactivé). Voir Délai tarpit ci-dessous. |
_WAF_TARPIT_DELAY_BLOCKED |
null |
Délai en secondes avant d’afficher le formulaire de déblocage (null = pas de délai supplémentaire). Voir Délai tarpit ci-dessous. |
_WAF_MESSAGE_403 |
(HTML minimal) | Corps HTML envoyé pour toutes les réponses de blocage 403. À surcharger pour personnaliser la page de blocage. |
_WAF_UNBLOCK_REDIRECT_URL |
/ |
URL de redirection après un déblocage réussi. À modifier si SPIP est dans un sous-dossier (par ex. /cms/). |
_WAF_RETENTION_BLOCKED_DAYS |
90 |
Nombre de jours de conservation des événements de requêtes bloquées en base de données. |
_WAF_RETENTION_BAN_DAYS |
180 |
Nombre de jours de conservation des événements de bannissement/déblocage en base de données. |
_WAF_EVENTS_DIR |
tmp/waf_events/ |
Répertoire des fichiers .jsonl d’événements différés (vidés en base par le cron). |
_WAF_FLUSH_BATCH_SIZE |
200 |
Nombre maximum de fichiers d’événements traités par exécution du cron. |
_WAF_DEBUG_ARCHIVE_EVENTS |
false |
Conserver les fichiers .jsonl vidés sous forme .archive au lieu de les supprimer. |
_WAF_EVENT_CONTEXT |
1000 |
Nombre de caractères de contexte conservés autour d’une violation lors de la journalisation (pour pouvoir l’inspecter). |
_WAF_BLOCKLISTS |
(FireHOL 1–3, Spamhaus DROP/DROPv6) | Tableau de définitions de listes. Ajouter des entrées pour étendre la couverture. |
_WAF_DEFAULT_BLOCKLISTS |
firehol_level1,firehol_level2 |
Identifiants activés par défaut avant que l’administrateur ne modifie la sélection, séparés par des virgules. |
_WAF_TI_CACHE_DIR |
tmp/waf_ti/ |
Répertoire pour les données de listes téléchargées. |
_WAF_TI_CACHE_TTL |
90000 (25 h) |
Durée de vie du cache pour les données de listes et les résultats de recherche par IP. |
_WAF_CACHE_BACKEND |
file |
Backend utilisé pour le cache chaud du WAF (compteurs de strikes, drapeaux de bannissement, config, résultats de listes publiques par IP). file ou apcu. Voir Backend de cache ci-dessous. |
_WAF_CACHE_DIR |
tmp/cache/waf/ |
Répertoire du backend de cache fichier. Utilisé uniquement quand _WAF_CACHE_BACKEND vaut file. |
_WAF_CSS |
(non défini) | CSS personnalisé pour la page du formulaire de déblocage. S’il est défini, utilisé dans une balise <style> inline à la place de la feuille de style par défaut. |
_WAF_PROFILE |
(non défini) | Définir à microtime(true) pour activer le profilage par requête. Voir Profilage. |
Délai tarpit
Les IP en liste de refus, sur une liste publique ou bloquées définitivement sont bloquées immédiatement par défaut. On peut ajouter un délai volontaire pour faire perdre du temps aux scanners, au prix d’un processus PHP maintenu ouvert pendant cette durée.
Désactivé par défaut. À activer avec prudence : si tous les processus PHP dorment en même temps, le site devient inaccessible. À éviter sur les serveurs avec peu de processus ou derrière des proxies avec des limites de connexion basses.
// Délai en secondes avant d'envoyer la 403 aux IP en liste de refus, sur liste publique ou bloquées définitivement (0 = désactivé)
define('_WAF_TARPIT_DELAY', 25);
Un délai séparé, plus court, peut être appliqué avant l’affichage du formulaire de déblocage pour les utilisateurs légitimes pris par un faux positif. Cela décourage les soumissions automatisées tout en gardant l’attente supportable pour les humains :
// Délai en secondes avant d'afficher le formulaire de déblocage (null = pas de délai)
define('_WAF_TARPIT_DELAY_BLOCKED', 4);
Quand _WAF_TARPIT_DELAY_BLOCKED vaut null (par défaut), aucun délai n’est appliqué avant le formulaire de déblocage, même si _WAF_TARPIT_DELAY est défini. Les deux délais sont indépendants.
Valeurs recommandées : _WAF_TARPIT_DELAY à 25 s ou plus (bots), _WAF_TARPIT_DELAY_BLOCKED entre 3 et 5 s (humains).
Le plugin envoie déjà une réponse 403 Forbidden au client. Si vous êtes derrière un reverse proxy, le plus sûr est d’implémenter le tarpit au niveau du proxy, en détectant la 403 et en retardant la réponse à ce niveau.
Backend de cache
Le WAF conserve un cache chaud des données lues fréquemment (compteurs de strikes, drapeaux de bannissement, config fusionnée et résultats de recherche de listes publiques par IP) afin de décider d’autoriser ou de bloquer une requête sans interroger la base de données à chaque visite.
Par défaut, ce cache utilise le backend file : de petits fichiers PHP dans tmp/cache/waf/. Sur une installation classique, ces fichiers sont maintenus en mémoire par OPcache, ce qui rend les lectures pratiquement en mémoire et très rapides.
Si OPcache est désactivé, ou tellement saturé que les fichiers de cache du WAF sont sans cesse évincés (ce qui provoque des lectures disque à chaque requête), vous pouvez faire stocker le cache du WAF en mémoire partagée APCu à la place :
// config/mes_options.php
define('_WAF_CACHE_BACKEND', 'apcu');
Prérequis et remarques :
- L’extension PHP APCu doit être installée et activée (
apcu.enabled=1, etapcu.enable_cli=1si vous exécutez les tâches cron du WAF en CLI).
Règles personnalisées
Le plugin expose un pipeline waf_custom_rules. Définissez votre fonction de règle dans mes_options.php et branchez-la sur le pipeline.
Bloquer un paramètre GET spécifique :
function waf_custom_rule__block_foo_bar($ctx) {
if (isset($ctx['get']['foo']) && $ctx['get']['foo'] === 'bar') {
waf_handle_violation('CUSTOM_RULE', 'foo=bar', $ctx['client_ip']);
}
}
$GLOBALS['spip_pipeline']['waf_custom_rules'] = '|waf_custom_rule__block_foo_bar';
Bloquer par ex. ?page=spipdf (quand le plugin SPIPDF n’est pas installé) :
function waf_custom_rule__block_spipdf($ctx) {
if (isset($ctx['get']['page']) && $ctx['get']['page'] === 'spipdf') {
waf_handle_violation('CUSTOM_RULE', 'blocked_param=page, value=spipdf', $ctx['client_ip']);
}
}
$GLOBALS['spip_pipeline']['waf_custom_rules'] .= '|waf_custom_rule__block_spipdf';
Désactiver des règles
Le pipeline waf_handle_violation permet d’ignorer des violations spécifiques sans contourner l’ensemble du WAF. L’IP passe toujours par les vérifications de liste de refus, de liste publique et de bannissement. Seul le déclenchement de la règle individuelle est ignoré.
Mettez $flux['data'] = true et renvoyez $flux pour ignorer la violation. La fonction reçoit le tableau flux standard de SPIP avec args (détails de la violation) et data (initialement false).
Exemple : autoriser les balises <script> lors de l’édition d’articles dans l’espace privé :
function waf_override__allow_script_in_articles($flux) {
$args = $flux['args'];
if (
isset($args['get']['exec']) && $args['get']['exec'] === 'article_edit'
&& $args['reason'] === 'PATTERN_MATCH'
&& (
$args['pattern'] == '/<script/i'
|| $args['pattern'] == '/<\/script>/i'
)
) {
$flux['data'] = true;
}
return $flux;
}
$GLOBALS['spip_pipeline']['waf_handle_violation'] = '|waf_override__allow_script_in_articles';
Contourner le WAF
Le pipeline waf_bypass permet au code externe de contourner tous les contrôles du WAF pour une requête, y compris les vérifications de liste de refus, de liste publique, de bannissement et de règles. Il s’exécute juste après la vérification de la liste autorisée. C’est utile pour désactiver complètement le WAF pour certaines requêtes.
Mettez $flux['data'] = true et renvoyez $flux pour contourner. La fonction reçoit le tableau flux standard de SPIP avec args (contenant client_ip) et data (initialement false).
Contourner le WAF pour les utilisateurs connectés (rédacteurs, administrateurs, etc.) :
function waf_bypass__logged_in($flux) {
if (!empty($GLOBALS['auteur_session']['id_auteur'])) {
$flux['data'] = true;
}
return $flux;
}
$GLOBALS['spip_pipeline']['waf_bypass'] .= '|waf_bypass__logged_in';
Ce code vérifie la variable $GLOBALS['auteur_session']['id_auteur'], qui est définie par SPIP après une connexion réussie.
Note : Ce pipeline est différent de
waf_handle_violation, qui ne fait qu’ignorer des violations de règles individuelles. Si une IP est déjà bannie ou sur une liste publique,waf_handle_violationne changera rien, car la requête est bloquée avant même que les règles ne s’exécutent. Utilisezwaf_bypassquand vous devez laisser passer quelqu’un quel que soit le statut de bannissement de son IP.
Profilage
Ajouter dans config/mes_options.php :
define('_WAF_PROFILE', microtime(true));
define('_LOG_FILTRE_GRAVITE', _LOG_INFO);
La deuxième ligne est nécessaire si _LOG_FILTRE_GRAVITE n’est pas défini, pour régler le bon niveau de log. Sinon, rien n’est écrit dans les logs.
Chaque requête ajoute une ligne dans tmp/log/waf.log :
WAF_PROFILE PASS total:3.42ms fn:1.18ms
WAF_PROFILE VIOLATION total:5.01ms fn:2.73ms
- total : temps entre le
define()et la sortie du WAF (inclut le bootstrap SPIP) - fn : temps passé dans
waf()uniquement - Résultats possibles :
PASS,ALLOWLISTED,BYPASS,NO_IP,DENYLISTED,BLOCKLISTED,HARDBLOCKED,BANNED,UNBLOCK_REDIRECT,VIOLATION
Retirez les deux lignes une fois le profilage terminé.
Debug : archiver les fichiers d’événements
Par défaut, les fichiers .jsonl d’événements sont supprimés après avoir été vidés en base de données. Pour les conserver pour inspection, activez l’archivage :
define('_WAF_DEBUG_ARCHIVE_EVENTS', true);
Les fichiers traités sont renommés en .archive dans le répertoire tmp/waf_events/ au lieu d’être supprimés. À désactiver en production. Les fichiers archivés s’accumulent et ne sont jamais nettoyés automatiquement.
Statistiques en ligne de commande
Statistiques pour un résultat donné (remplacez PASS par le résultat voulu) pour MIN, MOYENNE, MAX :
Pour les statistiques min, moyenne, max de base :
grep 'WAF_PROFILE PASS ' tmp/log/waf.log | grep -oP 'fn:\K[0-9.]+' | awk '{s+=$1;n++;if(!min||$1<min)min=$1;if($1>max)max=$1} END{printf "n=%d min=%.2fms avg=%.2fms max=%.2fms\n",n,min,s/n,max}'
Pour les percentiles (p50/p95/p99) : (Ce code bash s’exécute dans le répertoire racine de l’installation SPIP).
# aller dans le répertoire racine de l'installation SPIP :
cd /var/www/html
La boucle parcourt les résultats possibles et affiche les statistiques.
# Afficher les percentiles par résultat :
for outcome in PASS ALLOWLISTED NO_IP DENYLISTED BLOCKLISTED HARDBLOCKED BANNED UNBLOCK_REDIRECT VIOLATION; do
echo ""
echo "--- $outcome ---"
grep "WAF_PROFILE $outcome " tmp/log/waf.log \
| grep -oP 'fn:\K[0-9.]+' \
| sort -n \
| awk '{a[NR]=$1} END{
printf "n=%d p50=%.2fms p95=%.2fms p99=%.2fms\n",
NR,
a[int(NR*0.50)+1],
a[int(NR*0.95)+1],
a[int(NR*0.99)+1]
}'
done
Vue d’ensemble de tous les types de résultats dans le log :
grep -oP 'WAF_PROFILE \K\S+' tmp/log/waf.log | sort | uniq -c | sort -rn

Discussions by date of activity
One discussion
Bonjour,
Merci et BRAVO!
Petit demande pour en savoir plus... Plus rapidement...
Je fais un copié collé de l’adresse IP qui est bloquée sur WHOIS pour en savoir plus sur son propriétaire.
Pour exemple : Whois IP 102.220.161.89
Qui me donne un ensemble de données avec une adresse du Nigéria...
https://www.whois.com/whois/102.220.161.89
Serait-il possible d’avoir un bouton qui crée un automatisme d’envoi de l’adresse vers WHOIS et ouvre une fenêtre avec les informations ?
Bon courage et bonne journée.
Michel
Reply to this message
Add a comment
Avant de faire part d’un problème sur un plugin X, merci de lire ce qui suit :
Merci d’avance pour les personnes qui vous aideront !
Par ailleurs, n’oubliez pas que les contributeurs et contributrices ont une vie en dehors de SPIP.
Follow the comments:
|
