Sécuriser des sites SPIP

Bonjour à tous,

J’apprécie beaucoup SPIP, que je trouve très simple et d’un excellent rapport qualité-prix pour la mise en place et la maintenance de sites associatifs. Contrairement à WordPress, qui s’avère complexe à sécuriser et nécessite souvent de nombreux plugins payants, SPIP me semble particulièrement adapté aux besoins des associations.

Je configure des sites associatifs sur des hébergements mutualisés (IONOS, OVH, etc.) et je souhaite en optimiser la sécurité. Ces projets partagent les mêmes caractéristiques :

  • Un site de présentation avec des actualités et un calendrier d’événements.
  • Une maintenance assurée par quelques rédacteurs et un profil plus technique pour l’implémentation.
  • Un site configuré en « lecture seule », sans interactivité pour les visiteurs.

En plus de l’installation de l’écran de sécurité, j’ai déjà effectué les actions suivantes :

  • Suppression des formulaires dans les squelettes
  • Désactivation des fonctions d’interactivité dans la configuration de l’espace privé
  • Configuration des statistiques via Matomo sur une base de données distincte et un sous-domaine

Je sollicite vos conseils et retours d’expérience sur les bonnes pratiques pour la mise en place d’un fichier .htaccess / .htpasswd dans ce contexte.

L’idee est de proteger spip.php page « login » et de reset de mot de passe, mais aussi être compatible avec les URLs propres.

Voici la configuration que j’ai ​t​rouvé via de l’IA:

1 - ecrire/.htaccess

AuthType Basic
AuthName "Espace de redaction"
AuthUserFile /chemin/absolu/hors/racine/.htpasswd
Require valid-user

2 - le .htaccess - à la racine

SetEnvIfExpr "%{QUERY_STRING} =~ /(^|&)page=(login|spip_pass)/" zone_privee

<Files "spip.php">
  AuthType Basic
  AuthName "Espace de redaction"
  AuthUserFile /chemin/absolu/hors/racine/.htpasswd
  Require expr "%{ENV:zone_privee} == ''"
  Require valid-user
</Files>

3 - config/mes_options.php

$GLOBALS['ignore_auth_http'] = true;


  • URLs propres et protection du login. est-ce compatible avec les URLs propres ?
    Claude disait :

Avec les URLs propres, les pages publiques sont réécrites vers spip.php. Est-ce que la condition sur la query string reste fiable dans ce cas, ou risque-t-elle de protéger des pages publiques par effet de bord ? Faut-il doubler la règle sur REQUEST_URI ?

  • Nom exact de la page de reset:
    Claude disait :

Est-ce toujours spip_pass sur les versions 4.x, ou faut-il viser autre chose ?

  • ignore_auth_http. Est-ce suffisant à ou faut-il aussi $GLOBALS['ignore_remote_user'] = true; ?

  • Tâches CRON.
    Claude disait :

La doc recommande _DIRECT_CRON_FORCE quand le site est protégé par htaccess/htpasswd. Dans ce cas /ecrire/ et deux pages publiques sont protégés — spip.php?action=cron reste joignable. Cette constante est-elle nécessaire, ou est-ce que cela impose une latence inutile sur l’affichage des pages ?

Qu’en pensez-vous ? sur un site sans aucune interactivité publique

Merci d’avance pour votre aide.

Aurel

Cela me semble inutile de protéger l’accès à la page login car les attaques qu’on a observer sur spip 4.4.20 et moins ne sont pas passé par là (mais dans la page de recherche, vous pouvez vérifier vos logs)

Oui, comme Gilles l’a mentionné, les problèmes de sécurité ne sont pas liés uniquement à la page de connexion, ils peuvent apparaître partout dans le site, même dans le formulaire de recherche ou hors des formulaires…

Une approche plus efficace serait d’installer SPIP WAF , un plugin que j’ai développé avec le soutien de la communauté ici. Il bloque les requêtes malveillantes et les adresses IP connues pour des activités suspectes .

Normalement, il suffit d’installer le plugin comme d’habitude , d’aller sur sa page de configuration , puis de suivre les étapes pour sélectionner les en‑têtes appropriés selon ton hébergement.

Et, je suis également tout à fait d’accord concernant l’usage de SPIP par les associations qui ne souhaitent pas passer leur temps, ni leur budget, à « maintenir » des plugins qui se cassent à chaque mise à jour et qui coutent plus en plus chers. SPIP a une vraie force à faire valoir en matière de stabilité, de vision long terme et de simplicité d’usage…

Merci @ursrig pour le feedback et l’idee du WAF

Sur le associatif sous SPIP 4.4.15 en mutualisé, le plugin WAF y tourne depuis quelques jours.

Ca me semble tres bien :wink:

Je ne suis pas développeur, j’ai simplement donné à Claude un extrait de mon tableau de bord et une copie locale du dépôt git et le htaccess, en lui demandant ce qu’il en tirait sous forme de rapport pour le développeur, je vous partage la response, si ca peut être utile

EN tout cas, merci pour le plugin, c’est tres pour les asso avec peu de moyen masi avoir un outil simple de protection sans devoir €€€…

dans la config, ce qui est actif :

  • FireHOL niveau 1 (niv2 pas actif)
  • En-tête IP client : REMOTE_ADDR
  • Enregistrer les échecs de connexion comme strikes WAF
  • Désactiver les règles WAF pour les utilisateurs connectés (rédacteurs, administrateurs)

RAPPORT — Plugin SPIP WAF v1.2.18

Auteur : Claude (Anthropic), à la demande du webmestre du site
Date : 28 août 2026
Objet : couverture des règles de détection et points d’extension

1. Périmètre et méthode

Sources analysées :

  • Extraits du tableau de bord WAF d’un site en production, fenêtre de 7 jours
  • Lecture statique du code source, copie locale du dépôt git — version 1.2.18, paquet.xml généré le 2026-04-03, compatibilité [4.4.13;4.*]
  • Le .htaccess du site, afin de vérifier qu’aucun filtrage serveur ne masquait les constats

Limites, à lire avant les constats :

  • Analyse statique uniquement. Aucun test dynamique, aucune requête d’attaque rejouée. Les conclusions sur ce qui « passerait » sont déduites de la lecture des règles, pas observées.
  • Aucune modification appliquée sur le site audité.
  • L’arborescence complète de la racine web n’a pas été inspectée (copie locale partielle).
  • Il ne s’agit pas d’un test d’intrusion.

2. Télémétrie observée

Sur 7 jours : 273 événements, dont 268 blocages, sur 22 IP uniques.

  • 262 blocages attribués aux règles propres du plugin (16 IP)
  • 6 blocages via les listes publiques (6 IP) — FireHOL niveau 1 à 0 %
  • Une IP nettement dominante, 34.125.205.23 (plage Google Cloud), en bannissement long terme

Comportements identifiés :

  • SSRF vers les métadonnées cloud : /fetch?uri=http://169.254.169.254/latest/meta-data/iam/security-credentials/, avec variantes url=, target=, dest=, path=, proxy?url=
  • Balayage de fichiers sensibles : .env, aws/credentials, secrets.yaml, service-account.json, ssl/server.key, .git/config
  • RCE de frameworks tiers : ThinkPHP (?s=/Index/\think\app/…), Laravel Ignition (/_ignition/health-check)
  • Usurpation d’User-Agent : la même IP alterne GrokBot, Perplexity-User et TelegramBot avec des versions de Chrome légèrement différentes

3. Constats

C1 — Absence de règle SSRF / métadonnées cloud · Impact : moyen

Ni waf_malicious_payload_patterns() ni waf_cms_probe_patterns() ne contiennent de motif visant les services de métadonnées d’instance. L’unique occurrence de 169.254.0.0/16 est dans waf_ti_local_cidrs() (waf_fonctions.php:1325), qui protège le téléchargeur de listes du plugin lui-même — usage distinct, sans effet sur les requêtes entrantes.

Conséquence déduite : sur le site observé, ces requêtes n’ont été bloquées que parce que l’IP était déjà bannie pour d’autres motifs. Depuis une IP sans historique, elles ne devraient rencontrer aucune règle. Non vérifié par un test réel.

Si une règle devait être ajoutée, il paraît important de rester sur des chaînes très spécifiques — 169.254.169.254, metadata.google.internal, /latest/meta-data/, /computeMetadata/v1/ — et d’éviter le mot metadata seul, qui produirait des faux positifs dès qu’un visiteur l’emploie dans une recherche ou une contribution.

C2 — Sonde .env ancrée à la racine uniquement · Impact : moyen

Le motif est '/^' . $p . '\/\.env$/i', ancré en début de chemin et terminé par $. Les variantes observées y échappent : /api/.env, /backend/.env, /.env.bak, /.env.local. Ne sont pas couverts non plus /aws/credentials, /secrets.yaml, /service-account.json, /ssl/server.key, /.git/config.

Voir toutefois C6 : ce constat appelle sans doute une réponse au niveau serveur plutôt qu’au niveau PHP.

C3 — RCE de frameworks tiers absentes de la liste · Impact : faible à moyen

/_ignition/health-check (Laravel Ignition, CVE-2021-3129) et la chaîne ThinkPHP figurent parmi les sondes les plus fréquentes du trafic observé et ne correspondent à aucun motif existant. Ces chemins n’existant jamais sur une installation SPIP, le risque de faux positif d’une règle les visant est nul.

C4 — Le pipeline waf_custom_rules n’est pas déclaré · Impact : ergonomie développeur

Le pipeline est bien appelé (waf_fonctions.php:354) et documenté par un exemple en commentaire juste au-dessus. En revanche il n’apparaît pas dans paquet.xml, contrairement aux autres pipelines du plugin. Il n’est donc découvrable qu’en lisant le source.

C’est dommage, car c’est précisément le mécanisme qui permet à un webmestre d’ajouter ses propres motifs sans forker le plugin ni perdre ses règles à chaque mise à jour. Le déclarer et le mentionner sur la page contrib augmenterait nettement son usage.

Détail annexe : l’exemple en commentaire procède par affectation directe, ce qui écraserait un autre abonné éventuel. Une concaténation serait plus robuste :

$GLOBALS['spip_pipeline']['waf_custom_rules'] = ($GLOBALS['spip_pipeline']['waf_custom_rules'] ?? '') . '|ma_fonction';

C5 — Le tableau de bord surestime la couverture des règles · Impact : lisibilité

Le contrôle de bannissement s’exécute en waf_fonctions.php:300, donc avant les règles de contenu (ligne 356 et suivantes). Une fois une IP bannie, l’intégralité de son trafic est journalisée sous BANNED_IP, y compris des requêtes qu’aucune règle de contenu ne détecterait.

Le tableau donne alors l’impression d’une couverture plus large qu’elle ne l’est — c’est précisément ce qui a motivé la lecture du code dans le cas présent. Afficher, même en second plan, la règle qui aurait matché sur une requête déjà bloquée par bannissement donnerait une image fidèle de la couverture réelle et aiderait à repérer les angles morts.

C6 — Le WAF ne voit pas les requêtes servies directement par Apache · Impact : à documenter

Le .htaccess standard de SPIP court-circuite la réécriture lorsque le fichier demandé existe :

apache

RewriteCond %{REQUEST_FILENAME} -f
RewriteRule "." - [skip=100]

Dans ce cas Apache sert le fichier directement, SPIP n’est pas chargé, et le WAF ne s’exécute pas.

C’est inhérent à tout WAF applicatif en PHP et non un défaut d’implémentation, mais la conséquence mériterait d’être explicitée dans la documentation : le plugin ne protège pas les fichiers présents sur le disque, en particulier un script déposé après une compromission. Un webmestre peut raisonnablement croire l’inverse.

Cela invite aussi à réviser l’approche du constat C2 : les sondes de fichiers sensibles sont bloquées plus efficacement, et à moindre coût d’exécution, au niveau serveur qu’au niveau PHP. Fournir dans la documentation du plugin un extrait .htaccess recommandé — .env, .git/, *.sql, *.bak, fichiers de clés — serait un complément naturel.

À noter au passage : le .htaccess livré par SPIP bloque .svn/ mais pas .git/, héritage de l’époque Subversion, alors que /.git/config est aujourd’hui une des sondes les plus courantes.

4. Points solides relevés

L’architecture est saine et plusieurs choix méritent d’être soulignés.

  • Ordre des contrôles pertinent : allowlist, denylist manuelle, listes publiques, bannissement, puis règles de contenu. Les tests les moins coûteux d’abord.
  • Bypass des rédacteurs connectés (_WAF_BYPASS_LOGGED_IN, ligne 320) : évite qu’un article contenant system(reboot) fasse bannir son auteur, tout en maintenant les protections IP. C’est un piège classique des WAF, correctement traité ici.
  • Motifs de contenu prudents : les règles SQLi exigent des mots-clés structurels, la traversée de répertoire demande au moins deux ../ consécutifs, base64_decode n’est signalé que suivi d’une parenthèse. Le souci d’éviter les faux positifs est visible jusque dans les commentaires.
  • Journalisation différée (waf_flush_events, 300 s) et troncature du contexte à _WAF_EVENT_CONTEXT : protège la base d’un remplissage sous requêtes volumineuses.
  • Points d’extension nombreux : waf_bypass (l.207), waf_custom_rules (l.354), waf_on_ban (l.727), waf_handle_violation (l.999), waf_stats_extra.

5. Pistes, par rapport bénéfice / effort

  1. Déclarer waf_custom_rules dans paquet.xml et le documenter sur la page contrib (C4). Effort minimal, permet à chaque site de combler ses propres angles morts sans attendre une version du plugin.
  2. Documenter un extrait .htaccess complémentaire plutôt que d’ajouter les motifs de fichiers sensibles aux règles PHP (C6, C2). Plus efficace, quasiment gratuit, et lève au passage un malentendu sur le périmètre du plugin.
  3. Ajouter un jeu de motifs SSRF (C1), en restant sur des chaînes très spécifiques.
  4. Ajouter les sondes RCE de frameworks tiers (C3) : /_ignition/, invokefunction, think\app, /actuator/env.
  5. Enrichir le tableau de bord de la règle « virtuelle » sur les requêtes bloquées par bannissement (C5).

6. Ce qu’il resterait à vérifier

Points hors de portée d’une analyse statique, à confirmer par le mainteneur :

  • Comportement réel d’une requête SSRF émise depuis une IP sans historique de strikes — validerait ou infirmerait C1.
  • Efficacité des listes publiques : FireHOL niveau 1 à 0 % sur la fenêtre observée, sans qu’on puisse distinguer un problème de rafraîchissement d’une simple absence de recoupement avec le trafic reçu.
  • Détection d’usurpation d’User-Agent (bot déclaré hors des plages IP officielles). Piste évoquée mais non recommandée en l’état : le risque de faux positifs sur des crawlers légitimes est élevé et demanderait une évaluation à part entière.