# Proposition — wp-admin.php v3 : bannissement 3 jours + redirection + log

Date : 2026-08-16
Statut : **proposition, AUCUNE modification appliquée** (fichier actuel inchangé).
Fichier concerné : `/home/alan/SITE/WEB/wp-admin.php` (racine web sur le mutualisé).

---

## 1. Comportement demandé

1. Toute IP qui visite `wp-admin.php` **et ses variations** (wp-login.php, wp-admin/,
   xmlrpc.php, admin-ajax.php, wp-config.php) est **bannie 3 jours** dès la 1re visite.
2. Elle est **redirigée** vers une page HTML (`/banned.html`) précisant que l'IP
   est bannie pour 3 jours.
3. **Toutes les informations** sont enregistrées dans `logs/trapped_bots.log`
   (format JSONL existant conservé + lignes simples lisibles).
4. Toute requête ultérieure d'une IP bannie → redirection immédiate vers
   `banned.html` (sans re-bannissement, sans log dupliqué inutile).

---

## 2. Code proposé pour `wp-admin.php` (v3)

```php
<?php
/**
 * PIÈGE À BOTS - Fichier leurre WordPress (v3)
 * ============================================
 * Comportement :
 * 1. Toute IP qui visite wp-admin.php (ou ses variations) est bannie 3 jours
 * 2. Elle est redirigée vers /banned.html (page précisant le bannissement)
 * 3. Toutes les informations sont enregistrées dans logs/trapped_bots.log
 * 4. Toute requête ultérieure d'une IP bannie est redirigée vers banned.html
 */

// -----------------------------------------------------------------
// CONFIGURATION
// -----------------------------------------------------------------
$logDir       = __DIR__ . '/logs';
$logFile      = $logDir . '/trapped_bots.log';   // fichier de log principal
$banFile      = $logDir . '/bans.json';          // liste des IP bannies + expiration
$banDuration  = 3 * 24 * 3600;                   // 3 jours en secondes
$banPage      = '/banned.html';                  // page HTML de bannissement
$maxLogSize   = 5 * 1024 * 1024;                 // 5 Mo max par fichier de log

// Aucune erreur PHP ne doit fuiter dans les réponses
@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 -->');
}

// -----------------------------------------------------------------
// RÉCUPÉRATION DE L'IP RÉELLE
// -----------------------------------------------------------------
$trustedProxies = [
    // '203.0.113.1', // ex. : IP du proxy en amont (Cloudflare, OVH CDN, ...)
];

function isTrustedProxy(string $ip): bool
{
    global $trustedProxies;
    foreach ($trustedProxies as $proxy) {
        if (strpos($proxy, '/') !== false) {
            list($subnet, $bits) = explode('/', $proxy);
            $bits = (int)$bits;
            $ipLong = ip2long($ip);
            $mask = $bits === 0 ? 0 : (-1 << (32 - $bits));
            $mask = $mask & 0xFFFFFFFF;
            if (($ipLong & $mask) === (ip2long($subnet) & $mask)) {
                return true;
            }
        } elseif ($ip === $proxy) {
            return true;
        }
    }
    return false;
}

function getRealIp(): string
{
    $remote = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    if (!isTrustedProxy($remote)) {
        return $remote;
    }
    $headers = [
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_REAL_IP',
        'HTTP_CLIENT_IP',
        'HTTP_CF_CONNECTING_IP',
    ];
    foreach ($headers as $header) {
        if (!empty($_SERVER[$header])) {
            $ip = trim(explode(',', $_SERVER[$header])[0]);
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
                return $ip;
            }
        }
    }
    return $remote;
}

// -----------------------------------------------------------------
// COLLECTE DES INFORMATIONS SUR LE BOT
// -----------------------------------------------------------------
function safeSubstr(string $str, int $max): string
{
    if (function_exists('mb_substr')) {
        return mb_substr($str, 0, $max);
    }
    return substr($str, 0, $max);
}

function collectBotInfo(): array
{
    $ip = getRealIp();

    if (function_exists('getallheaders')) {
        $headers = getallheaders();
    } else {
        $headers = [];
        foreach ($_SERVER as $key => $value) {
            if (strpos($key, 'HTTP_') === 0 && is_string($value)) {
                $headers[str_replace('_', '-', substr($key, 5))] = $value;
            }
        }
    }

    return [
        'ip'           => $ip,
        'timestamp'    => date('Y-m-d H:i:s'),
        'unix_time'    => time(),
        'method'       => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
        'uri'          => $_SERVER['REQUEST_URI'] ?? 'UNKNOWN',
        'query_string' => $_SERVER['QUERY_STRING'] ?? '',
        'user_agent'   => safeSubstr($_SERVER['HTTP_USER_AGENT'] ?? 'UNKNOWN', 512),
        'referer'      => safeSubstr($_SERVER['HTTP_REFERER'] ?? 'DIRECT', 1024),
        'host'         => safeSubstr($_SERVER['HTTP_HOST'] ?? 'UNKNOWN', 255),
        'accept_lang'  => safeSubstr($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'UNKNOWN', 512),
        'post_data'    => !empty($_POST) ? safeSubstr((string)json_encode($_POST), 8192) : 'EMPTY',
        'headers'      => json_encode($headers),
        'port'         => $_SERVER['REMOTE_PORT'] ?? 0,
    ];
}

// -----------------------------------------------------------------
// LOGGING
// -----------------------------------------------------------------
function logBot(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);
    }

    // Troncature de sécurité : ligne JSON toujours valide (JSONL)
    $json = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if ($json === false || strlen($json) > 16384) {
        $minimal = [
            'ip'         => $info['ip'] ?? '',
            'timestamp'  => $info['timestamp'] ?? '',
            'unix_time'  => $info['unix_time'] ?? 0,
            'uri'        => safeSubstr($info['uri'] ?? '', 512),
            'user_agent' => safeSubstr($info['user_agent'] ?? '', 512),
        ];
        $json = json_encode($minimal, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    if ($json === false) {
        $json = '{}';
    }
    @file_put_contents($logFile, $json . "\n", FILE_APPEND | LOCK_EX);
}

function logBotSimple(array $info): void
{
    global $logFile;

    $line = sprintf(
        "[%s] IP: %-15s | ACTION: %s | UA: %s | URI: %s\n",
        $info['timestamp'],
        $info['ip'],
        $info['action_taken'] ?? '?',
        $info['user_agent'],
        $info['uri']
    );

    @file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
}

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

function saveBans(array $bans): void
{
    global $banFile;
    @file_put_contents($banFile, json_encode($bans, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), LOCK_EX);
}

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);
}

// -----------------------------------------------------------------
// DÉTECTION DES VARIATIONS DE wp-admin
// -----------------------------------------------------------------
function isWpAdminRequest(): bool
{
    $uri  = $_SERVER['REQUEST_URI'] ?? '';
    $path = parse_url($uri, PHP_URL_PATH) ?? $uri;

    $patterns = [
        'wp-admin.php',          // le leurre lui-même (toute query string)
        'wp-admin/',             // wp-admin/install.php, wp-admin/upgrade.php, ...
        'wp-login.php',
        'wp-config.php',
        'xmlrpc.php',
        'admin-ajax.php',
    ];

    foreach ($patterns as $p) {
        if (stripos($path, $p) !== false) {
            return true;
        }
    }
    return false;
}

// -----------------------------------------------------------------
// ACTION PRINCIPALE
// -----------------------------------------------------------------
$botInfo = collectBotInfo();
$ip      = $botInfo['ip'];

// 1. IP déjà bannie ? -> redirection immédiate vers la page de bannissement
if (isBanned($ip)) {
    $botInfo['action_taken'] = 'BANNED_REDIRECT';
    logBot($botInfo);
    logBotSimple($botInfo);
    header('Location: ' . $banPage, true, 302);
    exit;
}

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

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

---

## 3. Page `banned.html` (à créer à la racine du site)

```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>
```

---

## 4. Propositions de solutions complémentaires

| Solution | Avantage | Inconvénient |
|---|---|---|
| **.htaccess (mod_rewrite)** : bloquer `wp-admin.php`, `wp-login.php`, `xmlrpc.php` en 403 directement au niveau Apache | Zéro charge PHP, instantané | Nécessite Apache + mod_rewrite |
| **Fail2ban** (serveur) : surveiller les logs Apache et bannir au niveau firewall (iptables) | Bloque avant même d'atteindre PHP | Nécessite accès root, config serveur |
| **Cloudflare WAF / Firewall Rules** : règle « Block » sur les chemins WP | Protection en amont, IP masquée | Dépend de Cloudflare |
| **Cron de purge** : script quotidien qui nettoie `bans.json` des entrées expirées | Garde le fichier léger | Le PHP purge déjà automatiquement à la lecture |
| **Liste noire .htaccess** : `Deny from <IP>` générée depuis `bans.json` | Blocage au niveau Apache | Doit être régénérée, moins flexible |

**Recommandation** : garder le leurre PHP (log riche + ban 3 jours) **et** ajouter
une règle `.htaccess` en amont pour les chemins WP classiques — double protection,
le PHP ne sert alors que de filet de sécurité.

---

## 5. Règles .htaccess proposées (à déployer avec le fichier)

```apache
# Rediriger TOUS les scans WordPress vers le piège (bloc existant étendu)
RewriteRule ^wp-login\.php$ /wp-admin.php [R=302,L]
RewriteRule ^wp-admin/?$ /wp-admin.php [R=302,L]
RewriteRule ^xmlrpc\.php$ /wp-admin.php [R=302,L]
RewriteRule ^wp-content/plugins/revslider/ /wp-admin.php [R=302,L]

# Nouveaux leurres
RewriteRule ^wp-(login|admin|content|includes|cron|json|config) /wp-admin.php [R=302,L]
RewriteRule ^wp-content/ /wp-admin.php [R=302,L]
RewriteRule ^wp-includes/ /wp-admin.php [R=302,L]
RewriteRule ^(phpmyadmin|adminer|pma|backup\.zip|db\.sql|config\.php(\.bak|~|\.old|\.save)?|\.git|\.env|\.htpasswd) /wp-admin.php [R=302,L]
```

**Attention** : les règles `wp-content/` et `wp-includes/` sont plus larges —
vérifier qu'aucun chemin réel du site ne commence par ces préfixes avant déploiement.

---

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

1. Rédiger `wp-admin.php` complet dans `/tmp/opencode/wp-admin.php` (relecture
   ligne par ligne — pas de `php -l` possible sur le VPS, pas de PHP local).
2. Faire valider le code (ou lancer `php -l` sur le mutualisé si dispo).
3. Créer `banned.html` à la racine du site.
4. Remplacer le fichier sur le mutualisé (FTP `put` **sans `-E`**, jamais de
   `mirror --delete`).
5. Étendre le `.htaccess` du mutualisé pour rediriger les URLs leurres
   supplémentaires vers `wp-admin.php` (section 5).
