Avec des serveurs exposés sur internet, un flux constant de bots malveillants, de scrapers agressifs et d'attaquants tente d'accéder aux ressources du site. QuietCMS intègre un système de contrôle d'accès directement dans le back-office : blocage d'IPs individuelles, de sous-réseaux /24 entiers et de pays entiers, avec une liste blanche pour les adresses IP administratrices.
Architecture du système
Le filtrage repose entièrement sur la classe AccessLogger, qui étend ses fonctions de journalisation avec des méthodes de contrôle d'accès. Les données sont stockées dans deux fichiers JSON dans le répertoire content/logs/ :
blocked.json— IPs et pays bloqués, avec horodatage et motif optionneladmin_ips.json— liste blanche des adresses IP administratrices
Le format de blocked.json utilise le préfixe de sous-réseau comme clé pour les IPs :
{
"ips": {
"185.220.101": {
"reason": "scan agressif",
"ts": 1748513847
}
},
"countries": {
"RU": { "reason": "trafic non pertinent", "ts": 1748513900 },
"CN": { "reason": "", "ts": 1748513950 }
}
}
Blocage au niveau du routeur
Le filtrage s'applique dès l'entrée dans index.php, avant même que le routeur ne traite la requête. Cette approche garantit qu'une IP ou un pays bloqué ne consomme aucune ressource applicative :
(function () {
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if ($ip !== '' && AccessLogger::isBlocked($ip)) {
http_response_code(403);
exit('<!DOCTYPE html>...<h1>403 — Accès refusé</h1>...');
}
$cc = strtoupper($_SERVER['HTTP_CF_IPCOUNTRY']
?? $_SERVER['GEOIP_COUNTRY_CODE']
?? $_SERVER['HTTP_X_COUNTRY_CODE'] ?? '');
if ($cc !== '' && $cc !== 'XX' && AccessLogger::isCountryBlocked($cc)) {
http_response_code(403);
exit('...');
}
})();
Logique de blocage par sous-réseau /24
Lorsqu'une IP est bloquée, c'est son préfixe /24 (les trois premiers octets) qui est enregistré dans blocked.json, pas l'adresse complète. La méthode isBlocked() vérifie d'abord la correspondance exacte, puis le sous-réseau :
public static function isBlocked(string $ip): bool
{
$data = self::getBlocked();
$subnet = self::getSubnet($ip); // ex : "185.220.101"
return isset($data['ips'][$ip]) || isset($data['ips'][$subnet]);
}
Cette granularité est volontaire : les bots et attaquants opèrent souvent depuis des plages IP contiguës. Bloquer le /24 stoppe l'ensemble de la plage sans avoir à saisir des dizaines d'entrées individuelles.
Détection du pays
Le code pays est lu depuis les en-têtes HTTP dans l'ordre de priorité suivant :
HTTP_CF_IPCOUNTRY— fourni par Cloudflare en tant que proxyGEOIP_COUNTRY_CODE— fourni par le module Apachemod_geoipHTTP_X_COUNTRY_CODE— en-tête personnalisé (Nginx, reverse proxy)
Le code XX (adresse inconnue ou privée dans la nomenclature Cloudflare) est explicitement exclu du filtrage pour éviter de bloquer les accès depuis un réseau local ou un VPN.
Liste blanche des IPs administratrices
Pour éviter qu'un administrateur ne se bloque lui-même par erreur, les IPs enregistrées dans admin_ips.json bénéficient de deux protections :
- Exclues du journal d'accès — leurs requêtes ne sont pas enregistrées dans
access.log, ce qui évite de polluer les statistiques avec du trafic administrateur - Jamais bloquées — même si leur sous-réseau est dans la liste de blocage
// Dans AccessLogger::log()
if ($ip !== '' && self::isAdminIp($ip)) return; // exclut totalement l'IP admin
Interface back-office
Le back-office expose les trois outils depuis l'onglet Sécurité du journal d'accès (/admin/access-log.php?tab=securite) :
- IPs bloquées — liste des sous-réseaux bloqués avec leur motif et la date d'ajout, bouton de déblocage individuel
- Pays bloqués — sélecteur ISO 3166-1 alpha-2 pour ajouter un pays, liste des pays actuellement bloqués
- IPs admin — gestion de la liste blanche ; les IPs admin peuvent être des adresses complètes ou des préfixes de sous-réseau
Dans le journal (onglet Journal), chaque entrée affiche l'IP masquée (185.220.101.xxx) et un bouton Bloquer qui pré-remplit le formulaire de blocage avec le sous-réseau correspondant, en un seul clic.
Écriture atomique et cache en mémoire
Les deux fichiers JSON sont écrits de manière atomique via un fichier temporaire suivi d'un rename(), ce qui évite tout état corrompu en cas d'écriture concurrente. Les lectures sont mises en cache dans des propriétés statiques $_blockedCache et $_adminIpsCache pour éviter de relire les fichiers à chaque requête au sein d'une même exécution PHP. Le cache est invalidé lors de chaque modification.
Articles similaires
URL admin randomisée : sécurité par l'obscurité
Comment QuietCMS génère une URL admin aléatoire à l'installation pour réduire la surface d'attaque.
Chiffrement AES-256-GCM des données sensibles
QuietCMS utilise AES-256-GCM pour chiffrer les mots de passe SMTP au repos. Détails techniques de l'implémentation.
Sécuriser son site avec les en-têtes HTTP : HSTS, CSP et Trusted Types
HSTS, CSP, Trusted Types : trois en-têtes HTTP que Lighthouse réclame pour sécuriser un site, et comment QuietCMS les ac…