# Proposition — wp-admin.php v4.1 (minimal, corrigé) : ban 3 jours + redirection + log

Date : 2026-08-16
Statut : **proposition, AUCUNE modification appliquée**.
Révision v4.1 : corrections de sécurité intégrées (voir section 7).
Fichiers concernés :
- `/home/alan/SITE/WEB/wp-admin.php` (leurre, à réécrire en version minimale)
- `/home/alan/SITE/WEB/.htaccess` (règles de redirection wp-admin uniquement)
- `/home/alan/SITE/WEB/banned.html` (page de bannissement, à créer)
- Log : `/home/alan/SITE/WEB/logs/trapped_bots.log` (déjà lu par `admin/logs-viewer.php`)

---

## 1. Comportement demandé (strictement minimal)

1. **wp-admin.php** : uniquement
   - enregistrer l'IP qui visite le fichier (et ses variantes) dans le log existant,
   - bannir l'IP **3 jours**,
   - la rediriger vers la page de bannissement.
   - **Aucune autre fonction** : pas de fausses réponses, pas de tarpit, pas de
     collecte de credentials, pas de simulation de vulnérabilité.
2. **.htaccess** : uniquement rediriger les demandes vers `wp-admin` et ses
   variantes vers `wp-admin.php`. **Aucune autre règle ajoutée** (la règle
   `revslider` existante est retirée : ce n'est pas une variante de wp-admin).
3. **Logs** : dans `logs/trapped_bots.log` (fichier existant affiché par
   `admin/logs-viewer.php`), format JSONL compatible avec le viewer
   (champs : `ip`, `timestamp`, `unix_time`, `user_agent`, `uri`, `action_taken`).

---

## 2. Code proposé pour `wp-admin.php` (v4.1 — minimal, corrigé)

```php
<?php
/**
 * PIÈGE À BOTS - Fichier leurre WordPress (v4.1 - minimal)
 * ========================================================
 * Fonctions UNIQUES :
 * 1. Enregistrer l'IP qui visite wp-admin.php (et variantes) dans le log
 * 2. Bannir l'IP pendant 3 jours
 * 3. Rediriger vers la page de bannissement
 * Aucune autre fonction.
 */

// -----------------------------------------------------------------
// CONFIGURATION
// -----------------------------------------------------------------
$logDir      = __DIR__ . '/logs';
$logFile     = $logDir . '/trapped_bots.log';  // lu par admin/logs-viewer.php
$banFile     = $logDir . '/bans.json';         // IP bannies + expiration
$banDuration = 3 * 24 * 3600;                  // 3 jours en secondes
$banPage     = '/banned';                      // page de bannissement (sans .html : évite le 301 du .htaccess)
$maxLogSize  = 5 * 1024 * 1024;                // 5 Mo max par fichier de log

// Aucune erreur PHP ne doit fuiter
@ini_set('display_errors', '0');
@ini_set('display_startup_errors', '0');
@error_reporting(0);

// Création du dossier de logs s'il n'existe pas
if (!is_dir($logDir)) {
    @mkdir($logDir, 0755, true);
}

// Protection du dossier de logs (Apache 2.2 et 2.4)
$htaccessDeny = "# Acces au dossier de logs refuse\n"
    . "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n"
    . "<IfModule !mod_authz_core.c>\nOrder allow,deny\nDeny from all\n</IfModule>\n";
if (!file_exists($logDir . '/.htaccess')) {
    @file_put_contents($logDir . '/.htaccess', $htaccessDeny);
}
if (!file_exists($logDir . '/index.html')) {
    @file_put_contents($logDir . '/index.html', '<!-- silence is golden -->');
}

// -----------------------------------------------------------------
// IP RÉELLE (REMOTE_ADDR uniquement)
// -----------------------------------------------------------------
function getRealIp(): string
{
    return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}

// -----------------------------------------------------------------
// GESTION DES BANISSEMENTS
// -----------------------------------------------------------------
function loadBans(): array
{
    global $banFile;
    if (!file_exists($banFile)) {
        return [];
    }
    $bans = json_decode((string)@file_get_contents($banFile), true);
    return is_array($bans) ? $bans : [];
}

function saveBans(array $bans): void
{
    global $banFile;
    $fp = @fopen($banFile, 'c');
    if ($fp === false) {
        return;
    }
    @flock($fp, LOCK_EX);
    @ftruncate($fp, 0);
    @fwrite($fp, json_encode($bans, JSON_PRETTY_PRINT));
    @fflush($fp);
    @flock($fp, LOCK_UN);
    @fclose($fp);
}

function isBanned(string $ip): bool
{
    $bans = loadBans();
    if (!isset($bans[$ip])) {
        return false;
    }
    if (time() > $bans[$ip]) {
        // Ban expiré : purge automatique
        unset($bans[$ip]);
        saveBans($bans);
        return false;
    }
    return true;
}

function banIp(string $ip): void
{
    global $banDuration;
    $bans = loadBans();
    $bans[$ip] = time() + $banDuration;
    saveBans($bans);
}

// -----------------------------------------------------------------
// LOG (format JSONL compatible admin/logs-viewer.php)
// -----------------------------------------------------------------
function logBan(array $info): void
{
    global $logFile, $maxLogSize;

    // Rotation du fichier de log s'il est trop gros
    if (file_exists($logFile) && filesize($logFile) > $maxLogSize) {
        $rotatedFile = str_replace('.log', '_' . date('Y-m-d_His') . '.log', $logFile);
        @rename($logFile, $rotatedFile);
    }

    // JSON toujours valide même avec octets UTF-8 invalides (UA de bots)
    $json = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
    if ($json === false || strlen($json) > 16384) {
        // Repli minimal : l'IP ne doit JAMAIS être perdue
        $minimal = [
            'ip'           => $info['ip'] ?? '',
            'timestamp'    => $info['timestamp'] ?? '',
            'unix_time'    => $info['unix_time'] ?? 0,
            'uri'          => substr($info['uri'] ?? '', 0, 512),
            'user_agent'   => substr($info['user_agent'] ?? '', 0, 512),
            'action_taken' => $info['action_taken'] ?? '',
        ];
        $json = json_encode($minimal, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
    }
    if ($json === false) {
        $json = '{}';
    }
    @file_put_contents($logFile, $json . "\n", FILE_APPEND | LOCK_EX);
}

// -----------------------------------------------------------------
// ACTION PRINCIPALE
// -----------------------------------------------------------------
$ip = getRealIp();

$info = [
    'ip'           => $ip,
    'timestamp'    => date('Y-m-d H:i:s'),
    'unix_time'    => time(),
    'uri'          => substr($_SERVER['REQUEST_URI'] ?? 'UNKNOWN', 0, 1024),
    'user_agent'   => substr($_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN', 0, 512),
    'action_taken' => '',
];

// 1. IP déjà bannie ? -> log + redirection immédiate
if (isBanned($ip)) {
    $info['action_taken'] = 'BANNED_REDIRECT';
    logBan($info);
    header('Location: ' . $banPage, true, 302);
    exit;
}

// 2. Première visite : ban 3 jours + log + redirection
banIp($ip);
$info['action_taken'] = 'BANNED_3_DAYS';
$info['ban_expires']  = date('Y-m-d H:i:s', time() + $banDuration);
logBan($info);

header('Location: ' . $banPage, true, 302);
exit;
```

---

## 3. Règles `.htaccess` proposées (UNIQUEMENT wp-admin et variantes)

Remplacer le bloc « Rediriger les scans WordPress vers le piège » existant par :

```apache
# Rediriger les scans WordPress vers le piège (wp-admin et variantes uniquement)
RewriteRule ^wp-login\.php$ /wp-admin.php [R=302,L]
RewriteRule ^wp-admin/?$ /wp-admin.php [R=302,L]
RewriteRule ^wp-admin/ /wp-admin.php [R=302,L]
RewriteRule ^wp-config\.php$ /wp-admin.php [R=302,L]
RewriteRule ^xmlrpc\.php$ /wp-admin.php [R=302,L]
RewriteRule ^admin-ajax\.php$ /wp-admin.php [R=302,L]
RewriteRule ^wp-cron\.php$ /wp-admin.php [R=302,L]
RewriteRule ^wp-json/ /wp-admin.php [R=302,L]
RewriteRule ^wp-content/ /wp-admin.php [R=302,L]
RewriteRule ^wp-includes/ /wp-admin.php [R=302,L]
```

**Notes importantes** :
- **Pas de règle sur `wp-admin.php` lui-même** : le fichier leurre gère ses
  propres requêtes (sinon boucle de redirection infinie).
- **Ancrage obligatoire** : `^wp-admin/?$` (exact) **et** `^wp-admin/`
  (sous-chemins). Ne JAMAIS écrire `^wp-admin/?` sans `$` : le motif matcherait
  aussi `wp-admin.php` → boucle de redirection infinie, le leurre ne serait
  jamais exécuté.
- La règle `revslider` existante est **retirée** (ce n'est pas une variante de
  wp-admin, et la demande est « pas d'autres fonctions »).
- **Vérifié** : aucun dossier `wp-content/` ni `wp-includes/` réel sur le site
  (vérifié le 2026-08-16) → les règles sont sûres.
- Le reste du `.htaccess` (HTTPS, suppression d'extensions, 404) est **inchangé**.

---

## 4. Page `banned.html` (à créer à la racine)

```html
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<title>Accès refusé - IP bannie</title>
<style>
  body { font-family: Arial, sans-serif; background: #1a1a1a; color: #eee;
         display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
  .box { text-align: center; max-width: 600px; padding: 40px; background: #2a2a2a;
         border: 1px solid #c0392b; border-radius: 8px; }
  h1 { color: #e74c3c; }
</style>
</head>
<body>
  <div class="box">
    <h1>Accès refusé</h1>
    <p>Votre adresse IP a été bannie de ce site pour une durée de <strong>3 jours</strong>
       suite à une tentative d'accès non autorisée.</p>
    <p>Si vous pensez qu'il s'agit d'une erreur, contactez l'administrateur du site.</p>
  </div>
</body>
</html>
```

**Note** : `$banPage` pointe vers `/banned` (sans `.html`) : le `.htaccess`
existant fait un 301 `/banned.html` → `/banned` (suppression d'extension) ;
pointer directement sur `/banned` évite la chaîne 302 → 301 → page.

---

## 5. Compatibilité avec `admin/logs-viewer.php`

- Le viewer lit `logs/trapped_bots.log` (JSONL) et affiche les colonnes :
  Date/Heure, IP, User-Agent, URI, Action.
- Les lignes écrites par `logBan()` contiennent exactement ces champs
  (`action_taken` = `BANNED_3_DAYS` ou `BANNED_REDIRECT`) → **aucune
  modification du viewer nécessaire**.
- Les lignes non-JSON sont ignorées par le viewer (aucun risque).

---

## 6. Déploiement prévu (à faire après validation)

1. Rédiger `wp-admin.php` v4.1 dans `/tmp/opencode/wp-admin.php` (relecture ligne
   par ligne — pas de `php -l` possible sur le VPS).
2. Créer `banned.html` à la racine du site.
3. Remplacer `wp-admin.php` sur le mutualisé (FTP `put` **sans `-E`**, jamais de
   `mirror --delete`).
4. Remplacer le bloc de redirection dans `.htaccess` (section 3).
5. Tester : visiter `/wp-admin.php` et `/wp-login.php` → redirection vers
   `banned.html`, entrée visible dans `admin/logs-viewer.php`.

---

## 7. Corrections de sécurité intégrées (v4 → v4.1)

| # | Problème | Gravité | Correction |
|---|---|---|---|
| 1 | `^wp-admin/?` sans `$` matche `wp-admin.php` → boucle de redirection infinie, leurre jamais exécuté | **Critique** | `^wp-admin/?$` (exact) + `^wp-admin/` (sous-chemins) |
| 2 | `json_encode` échoue sur UA avec octets UTF-8 invalides → ligne `{}`, **IP perdue** | **Important** | `JSON_INVALID_UTF8_SUBSTITUTE` + repli minimal contenant toujours l'IP |
| 3 | Pas de rotation du log → fichier illimité, `logs-viewer.php` lit tout en mémoire | Moyen | Rotation 5 Mo réintroduite (comme v1) |
| 4 | Course sur `bans.json` sans verrou → ban perdu en cas de requêtes simultanées | Moyen | `fopen('c')` + `flock(LOCK_EX)` + `ftruncate` + `fwrite` |
| 5 | Chaîne 302 → 301 → page pour `banned.html` | Faible | `$banPage = '/banned'` directement |
| 6 | `REQUEST_URI` non tronquée (query strings très longues) | Faible | Troncature à 1024 |

## 8. Analyse du script corrigé (v4.1)

### Points sûrs (vérifiés)
- **IP non spoofable** : `REMOTE_ADDR` uniquement, aucun en-tête X-Forwarded-For accepté.
- **Aucun input utilisateur dans un chemin de fichier** : `$logFile`, `$banFile`,
  `$banPage` sont des constantes → anti path traversal, anti header-injection.
- **Aucune fonction dangereuse** : pas de `eval`, `exec`, `system`, `shell_exec`,
  `include`/`require` dynamique, `unlink`.
- **Dossier logs protégé** : `.htaccess` deny + `index.html` vide, recréés à
  chaque requête s'ils disparaissent.
- **`exit` après chaque `header()`** : pas de fuite de contenu après redirection.
- **Erreurs PHP masquées** (`display_errors=0`, `error_reporting(0)`) : le leurre
  ne révèle rien.
- **Compatibilité viewer** : champs `ip`, `timestamp`, `unix_time`, `uri`,
  `user_agent`, `action_taken` → colonnes existantes de `logs-viewer.php`.
- **Pas de boucle** : `wp-admin.php` n'est pas redirigé par le .htaccess (aucune
  règle ne le matche), il s'exécute et redirige vers `/banned` (page statique).

### Points de vigilance restants (acceptables)
- **`bans.json` lisible par le viewer ?** Non : le viewer ne lit que
  `trapped_bots.log`. `bans.json` est dans `logs/` protégé par .htaccess deny.
- **Croissance de `bans.json`** : purge uniquement au retour d'une IP expirée.
  Acceptable (une entrée par IP, ~50 octets). Un cron de purge est possible en
  option (voir section 4 du document v3).
- **`flock` sur NFS/mutualisé** : peut être non fiable sur certains hébergeurs,
  mais reste une amélioration par rapport à l'écriture sans verrou.
- **`fopen('c')`** : mode « c » (create) disponible depuis PHP 5.2.6 — OK sur
  tous les hébergeurs modernes.
- **Pas de `php -l` possible** : pas de PHP local sur le VPS. La relecture ligne
  par ligne avant déploiement est obligatoire (étape 1 de la section 6).
