Intermediaire 2 min de lecture · 263 mots

Sécurité WordPress : Protéger son site contre les attaques courantes

Estimated reading time: 1 minute

Introduction

La sécurité WordPress n’est pas optionnelle. Avec plus de 43% des sites web utilisant WordPress, il est devenu une cible privilégiée des attaquants. Chaque jour, des dizaines de milliers de tentatives d’intrusion sont menées contre des sites WordPress mal sécurisés.

Ce guide complet vous apprendra à identifier, comprendre et contrer les vecteurs d’attaque les plus courants, avec des exemples de code production-ready et des stratégies de défense en profondeur.

Table des matières

  • Comprendre le modèle de menaces WordPress
  • Injection SQL et sécurisation des requêtes
  • Cross-Site Scripting (XSS)
  • Cross-Site Request Forgery (CSRF)
  • Authentification et gestion des sessions
  • Upload de fichiers sécurisé
  • Permissions et capabilities
  • Sécurité de l’API REST
  • Hardening du serveur et configuration
  • Monitoring et détection d’intrusions
  • 1. Comprendre le modèle de menaces WordPress

    Les vecteurs d’attaque principaux

┌─────────────────────────────────────────────────┐
│           VECTEURS D'ATTAQUE WORDPRESS          │
├─────────────────────────────────────────────────┤
│                                                 │
│  1. Injection SQL          ████████░░  80%     │
│  2. XSS                    ███████░░░  70%     │
│  3. CSRF                   ██████░░░░  60%     │
│  4. Brute Force            █████████░  90%     │
│  5. File Inclusion         ████░░░░░░  40%     │
│  6. Upload malveillant     █████░░░░░  50%     │
│  7. Vulnérabilités plugins ██████████  95%     │
│                                                 │
└─────────────────────────────────────────────────┘

Architecture de défense en profondeur


  Stratégie de sécurité multi-couches
 /
class YAWCSecurityFramework {

    /
      Couches de sécurité
     /
    const LAYERS = array(
        'server'         => 'Configuration serveur et firewall',
        'application'    => 'Code WordPress sécurisé',
        'authentication' => 'Authentification forte',
        'authorization'  => 'Contrôle d'accès',
        'validation'     => 'Validation des entrées',
        'sanitization'   => 'Nettoyage des données',
        'escaping'       => 'Échappement des sorties',
        'logging'        => 'Journalisation des événements',
    );

    /
      Initialiser toutes les couches de sécurité
     /
    public static function init() {
        // Couche 1: Sécurité serveur
        self::setupserversecurity();

        // Couche 2: Sécurité application
        self::setupapplicationsecurity();

        // Couche 3: Authentification
        self::setupauthenticationsecurity();

        // Couche 4: Autorisation
        self::setupauthorizationsecurity();

        // Couche 5: Validation & Sanitization
        self::setupdatasecurity();

        // Couche 6: Logging & Monitoring
        self::setupmonitoring();
    }

    private static function setupserversecurity() {
        // Headers de sécurité
        addaction('sendheaders', array(CLASS, 'addsecurityheaders'));

        // Désactiver l'affichage des erreurs en production
        if (!WPDEBUG) {
            iniset('displayerrors', 0);
            iniset('displaystartuperrors', 0);
        }
    }

    public static function addsecurityheaders() {
        if (!isadmin()) {
            // Protection XSS
            header('X-XSS-Protection: 1; mode=block');

            // Prévenir le MIME sniffing
            header('X-Content-Type-Options: nosniff');

            // Prévenir le clickjacking
            header('X-Frame-Options: SAMEORIGIN');

            // Content Security Policy
            header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';");

            // Référer Policy
            header('Referrer-Policy: strict-origin-when-cross-origin');

            // Permissions Policy
            header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
        }
    }

    private static function setupapplicationsecurity() {
        // Désactiver l'édition de fichiers
        if (!defined('DISALLOWFILEEDIT')) {
            define('DISALLOWFILEEDIT', true);
        }

        // Limiter les révisions
        if (!defined('WPPOSTREVISIONS')) {
            define('WPPOSTREVISIONS', 5);
        }

        // Désactiver le XML-RPC si non utilisé
        addfilter('xmlrpcenabled', 'returnfalse');

        // Retirer les informations de version
        removeaction('wphead', 'wpgenerator');
    }

    private static function setupauthenticationsecurity() {
        // Limiter les tentatives de connexion
        addaction('wploginfailed', array(CLASS, 'logfailedlogin'));

        // Forcer les mots de passe forts
        addaction('userprofileupdateerrors', array(CLASS, 'validatepasswordstrength'), 0, 3);

        // Expirer les sessions inactives
        addfilter('authcookieexpiration', array(CLASS, 'setsessiontimeout'), 10, 3);
    }

    private static function setupauthorizationsecurity() {
        // Vérifier les capacités sur les actions sensibles
        addaction('admininit', array(CLASS_, 'checkadmincapabilities'));
    }

    private static function setupdatasecurity() {
        // Validation et sanitization seront détaillées dans les sections suivantes
    }

    private static function setupmonitoring() {
        // Logging sera détaillé dans la section 10
    }

    public static function logfailedlogin($username) {
        $ip = self::getclientip();

        errorlog(sprintf(
            '[SECURITY] Failed login attempt for user "%s" from IP %s at %s',
            $username,
            $ip,
            currenttime('mysql')
        ));

        // Incrémenter le compteur de tentatives
        $attemptskey = 'loginattempts' . $ip;
        $attempts = gettransient($attemptskey) ?: 0;
        $attempts++;

        settransient($attemptskey, $attempts, HOURINSECONDS);

        // Bloquer après 5 tentatives
        if ($attempts >= 5) {
            self::blockip($ip, 'Too many failed login attempts');
            wpdie('Trop de tentatives de connexion échouées. Votre IP a été temporairement bloquée.');
        }
    }

    public static function validatepasswordstrength($errors, $update, $user) {
        if (empty($POST['pass1'])) {
            return;
        }

        $password = $POST['pass1'];

        // Critères de mot de passe fort
        $strengthrequirements = array(
            'length'    => strlen($password) >= 12,
            'uppercase' => pregmatch('/[A-Z]/', $password),
            'lowercase' => pregmatch('/[a-z]/', $password),
            'number'    => pregmatch('/[0-9]/', $password),
            'special'   => pregmatch('/[^A-Za-z0-9]/', $password),
        );

        $failedrequirements = array();

        if (!$strengthrequirements['length']) {
            $failedrequirements[] = 'au moins 12 caractères';
        }
        if (!$strengthrequirements['uppercase']) {
            $failedrequirements[] = 'une majuscule';
        }
        if (!$strengthrequirements['lowercase']) {
            $failedrequirements[] = 'une minuscule';
        }
        if (!$strengthrequirements['number']) {
            $failedrequirements[] = 'un chiffre';
        }
        if (!$strengthrequirements['special']) {
            $failedrequirements[] = 'un caractère spécial';
        }

        if (!empty($failedrequirements)) {
            $errors->add(
                'weakpassword',
                sprintf(
                    'Le mot de passe doit contenir %s.',
                    implode(', ', $failedrequirements)
                )
            );
        }

        // Vérifier contre les mots de passe communs
        if (self::iscommonpassword($password)) {
            $errors->add('commonpassword', 'Ce mot de passe est trop commun. Veuillez en choisir un autre.');
        }
    }

    private static function iscommonpassword($password) {
        $commonpasswords = array(
            'password', '12345678', 'qwerty', 'abc123', 'password123',
            'admin', 'letmein', 'welcome', 'monkey', '1234567890',
        );

        return inarray(strtolower($password), $commonpasswords);
    }

    public static function setsessiontimeout($expiration, $userid, $remember) {
        // 2 heures si "se souvenir de moi" n'est pas coché
        return $remember ? $expiration : 2  HOURINSECONDS;
    }

    public static function getclientip() {
        $ipkeys = array(
            'HTTPCLIENTIP',
            'HTTPXFORWARDEDFOR',
            'HTTPXFORWARDED',
            'HTTPXCLUSTERCLIENTIP',
            'HTTPFORWARDEDFOR',
            'HTTPFORWARDED',
            'REMOTEADDR'
        );

        foreach ($ipkeys as $key) {
            if (arraykeyexists($key, $SERVER) === true) {
                foreach (explode(',', $SERVER[$key]) as $ip) {
                    $ip = trim($ip);

                    if (filtervar($ip, FILTERVALIDATEIP, FILTERFLAGNOPRIVRANGE | FILTERFLAGNORESRANGE) !== false) {
                        return $ip;
                    }
                }
            }
        }

        return isset($SERVER['REMOTEADDR']) ? $SERVER['REMOTEADDR'] : '0.0.0.0';
    }

    private static function blockip($ip, $reason) {
        // Stocker l'IP bloquée
        $blockedips = getoption('yawcblockedips', array());

        $blockedips[$ip] = array(
            'reason'    => $reason,
            'timestamp' => currenttime('timestamp'),
            'expires'   => currenttime('timestamp') + HOURINSECONDS,
        );

        updateoption('yawcblockedips', $blockedips);
    }

    public static function checkadmincapabilities() {
        // Vérifier que l'utilisateur a bien les droits pour les actions sensibles
        if (isset($GET['action']) && $GET['action'] === 'delete') {
            if (!currentusercan('deleteposts')) {
                wpdie('Vous n'avez pas les permissions nécessaires.');
            }
        }
    }
}

// Initialiser le framework
addaction('init', array('YAWCSecurityFramework', 'init'));

2. Injection SQL et sécurisation des requêtes

Comprendre les injections SQL


  VULNÉRABLE : N'UTILISEZ JAMAIS CE CODE
 /
function yawcgetuserbyemailvulnerable($email) {
    global $wpdb;

    // DANGER : Injection SQL possible
    $query = "SELECT  FROM {$wpdb->users} WHERE useremail = '$email'";
    $user = $wpdb->getrow($query);

    return $user;
}

// Attaque possible :
// yawcgetuserbyemailvulnerable("' OR '1'='1' --");
// Résultat : Récupère tous les utilisateurs !

/
  SÉCURISÉ : Toujours utiliser prepare()
 /
function yawcgetuserbyemailsecure($email) {
    global $wpdb;

    // Utiliser prepare() pour échapper automatiquement
    $query = $wpdb->prepare(
        "SELECT  FROM {$wpdb->users} WHERE useremail = %s",
        $email
    );

    $user = $wpdb->getrow($query);

    return $user;
}

Requêtes préparées avancées


  Exemples de requêtes sécurisées
 /
class YAWCSecureDatabase {

    /
      Recherche sécurisée avec LIKE
     /
    public static function searchposts($searchterm) {
        global $wpdb;

        // Échapper les caractères spéciaux LIKE
        $searchterm = '%' . $wpdb->esclike($searchterm) . '%';

        $query = $wpdb->prepare(
            "SELECT ID, posttitle FROM {$wpdb->posts}
             WHERE posttitle LIKE %s
             AND poststatus = 'publish'
             AND posttype = 'post'
             ORDER BY postdate DESC
             LIMIT 20",
            $searchterm
        );

        return $wpdb->getresults($query);
    }

    /
      Requête avec plusieurs paramètres
     /
    public static function getpostsbyauthorandcategory($authorid, $categoryid, $limit = 10) {
        global $wpdb;

        $query = $wpdb->prepare(
            "SELECT p. FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->termrelationships} tr ON p.ID = tr.objectid
             INNER JOIN {$wpdb->termtaxonomy} tt ON tr.termtaxonomyid = tt.termtaxonomyid
             WHERE p.postauthor = %d
             AND tt.termid = %d
             AND p.poststatus = 'publish'
             AND p.posttype = 'post'
             ORDER BY p.postdate DESC
             LIMIT %d",
            absint($authorid),
            absint($categoryid),
            absint($limit)
        );

        return $wpdb->getresults($query);
    }

    /
      Insertion sécurisée
     /
    public static function insertcustomdata($userid, $datatype, $datavalue) {
        global $wpdb;

        $tablename = $wpdb->prefix . 'customdata';

        // Utiliser wpdb->insert() pour l'échappement automatique
        $result = $wpdb->insert(
            $tablename,
            array(
                'userid'    => absint($userid),
                'datatype'  => sanitizekey($datatype),
                'datavalue' => sanitizetextfield($datavalue),
                'createdat' => currenttime('mysql'),
            ),
            array('%d', '%s', '%s', '%s') // Types de données
        );

        if ($result === false) {
            errorlog('Database insert failed: ' . $wpdb->lasterror);
            return false;
        }

        return $wpdb->insertid;
    }

    /
      Mise à jour sécurisée
     /
    public static function updatecustomdata($id, $newvalue) {
        global $wpdb;

        $tablename = $wpdb->prefix . 'customdata';

        $result = $wpdb->update(
            $tablename,
            array(
                'datavalue'  => sanitizetextfield($newvalue),
                'updatedat'  => currenttime('mysql'),
            ),
            array('id' => absint($id)),
            array('%s', '%s'), // Types de données pour les valeurs
            array('%d')        // Types de données pour WHERE
        );

        return $result !== false;
    }

    /
      Suppression sécurisée
     /
    public static function deletecustomdata($id) {
        global $wpdb;

        $tablename = $wpdb->prefix . 'customdata';

        // Vérifier que l'utilisateur a le droit de supprimer
        if (!currentusercan('deleteposts')) {
            return false;
        }

        $result = $wpdb->delete(
            $tablename,
            array('id' => absint($id)),
            array('%d')
        );

        return $result !== false;
    }

    /
      Requête avec IN clause
     /
    public static function getpostsbyids($postids) {
        global $wpdb;

        // Nettoyer et valider les IDs
        $postids = arraymap('absint', (array) $postids);
        $postids = arrayfilter($postids); // Retirer les 0

        if (empty($postids)) {
            return array();
        }

        // Créer les placeholders
        $placeholders = implode(',', arrayfill(0, count($postids), '%d'));

        $query = $wpdb->prepare(
            "SELECT  FROM {$wpdb->posts}
             WHERE ID IN ($placeholders)
             AND poststatus = 'publish'",
            $postids
        );

        return $wpdb->getresults($query);
    }

    /
      Transaction sécurisée
     /
    public static function transferpostownership($postid, $fromuserid, $touserid) {
        global $wpdb;

        // Vérifier les permissions
        if (!currentusercan('editothersposts')) {
            return new WPError('permissiondenied', 'Permission refusée');
        }

        // Démarrer la transaction
        $wpdb->query('START TRANSACTION');

        try {
            // Mettre à jour le post
            $result1 = $wpdb->update(
                $wpdb->posts,
                array('postauthor' => absint($touserid)),
                array(
                    'ID'          => absint($postid),
                    'postauthor' => absint($fromuserid),
                ),
                array('%d'),
                array('%d', '%d')
            );

            if ($result1 === false) {
                throw new Exception('Échec de mise à jour du post');
            }

            // Logger le changement
            $result2 = $wpdb->insert(
                $wpdb->prefix . 'ownershiplog',
                array(
                    'postid'      => absint($postid),
                    'fromuserid' => absint($fromuserid),
                    'touserid'   => absint($touserid),
                    'changedat'   => currenttime('mysql'),
                ),
                array('%d', '%d', '%d', '%s')
            );

            if ($result2 === false) {
                throw new Exception('Échec du logging');
            }

            // Valider la transaction
            $wpdb->query('COMMIT');

            return true;

        } catch (Exception $e) {
            // Annuler en cas d'erreur
            $wpdb->query('ROLLBACK');

            errorlog('Transaction failed: ' . $e->getMessage());

            return new WPError('transactionfailed', $e->getMessage());
        }
    }
}

Validation et sanitization des entrées SQL


  Fonctions de validation pour les requêtes
 /
class YAWCSQLValidator {

    /
      Valider un ORDER BY
     /
    public static function validateorderby($orderby, $allowedcolumns) {
        $orderby = sanitizesqlorderby($orderby);

        if (empty($orderby)) {
            return 'postdate';
        }

        // Extraire la colonne
        $orderbyparts = explode(' ', $orderby);
        $column = $orderbyparts[0];

        // Vérifier contre la liste blanche
        if (!inarray($column, $allowedcolumns, true)) {
            return 'postdate';
        }

        return $orderby;
    }

    /
      Valider une direction de tri
     /
    public static function validateorder($order) {
        $order = strtoupper($order);

        return inarray($order, array('ASC', 'DESC'), true) ? $order : 'DESC';
    }

    /
      Créer une requête de recherche sécurisée
     /
    public static function buildsearchquery($searchterm, $searchfields) {
        global $wpdb;

        if (empty($searchterm)) {
            return '';
        }

        $searchterm = '%' . $wpdb->esclike($searchterm) . '%';
        $searchconditions = array();

        foreach ($searchfields as $field) {
            $searchconditions[] = $wpdb->prepare(
                "$field LIKE %s",
                $searchterm
            );
        }

        return '(' . implode(' OR ', $searchconditions) . ')';
    }
}

/
  Utilisation
 /
function yawcadvancedpostsearch($params) {
    global $wpdb;

    $defaults = array(
        'search'  => '',
        'orderby' => 'postdate',
        'order'   => 'DESC',
        'limit'   => 20,
        'offset'  => 0,
    );

    $params = wpparseargs($params, $defaults);

    // Valider les paramètres
    $allowedorderby = array('postdate', 'posttitle', 'commentcount', 'ID');
    $orderby = YAWCSQLValidator::validateorderby($params['orderby'], $allowedorderby);
    $order = YAWCSQLValidator::validateorder($params['order']);
    $limit = absint($params['limit']);
    $offset = absint($params['offset']);

    // Construire la condition de recherche
    $searchfields = array('posttitle', 'postcontent', 'postexcerpt');
    $searchcondition = YAWCSQLValidator::buildsearchquery($params['search'], $searchfields);

    // Construire la requête finale
    $whereclause = "poststatus = 'publish' AND posttype = 'post'";

    if (!empty($searchcondition)) {
        $whereclause .= " AND $searchcondition";
    }

    $query = "SELECT  FROM {$wpdb->posts}
              WHERE $whereclause
              ORDER BY $orderby $order
              LIMIT $limit OFFSET $offset";

    return $wpdb->getresults($query);
}

3. Cross-Site Scripting (XSS)

Comprendre XSS

Le XSS permet à un attaquant d’injecter du JavaScript malveillant qui s’exécutera dans le navigateur des visiteurs.


  VULNÉRABLE : N'utilisez jamais ce code
 /
function yawcdisplaycommentvulnerable($comment) {
    // DANGER : Le contenu du commentaire est affiché sans échappement
    echo '
'; echo '

' . $comment->commentcontent . '

'; echo '
'; } // Si un attaquant poste : // Le script s'exécutera dans le navigateur de chaque visiteur ! / SÉCURISÉ : Toujours échapper les sorties / function yawcdisplaycommentsecure($comment) { echo '
'; echo '

' . eschtml($comment->commentcontent) . '

'; echo '
'; }

Fonctions d’échappement WordPress


  Guide complet des fonctions d'échappement
 /
class YAWCEscapingGuide {

    /
      eschtml() : Pour le texte dans HTML
     /
    public static function exampleeschtml() {
        $userinput = '';

        // Convertit les caractères spéciaux en entités HTML
        echo '

' . eschtml($userinput) . '

'; // Affiche : <script>alert("XSS")</script> } / escattr() : Pour les attributs HTML / public static function exampleescattr() { $userinput = '" onclick="alert('XSS')'; // Sécurise les guillemets et caractères spéciaux echo ''; } / escurl() : Pour les URLs / public static function exampleescurl() { $userurl = 'javascript:alert("XSS")'; // Valide et nettoie l'URL echo 'Lien'; // Le javascript: sera retiré } / escjs() : Pour JavaScript / public static function exampleescjs() { $userinput = '"; alert("XSS"); var x="'; echo ''; } / wpkses() : Pour HTML autorisé / public static function examplewpkses() { $usercontent = '

Texte gras

'; // Autoriser seulement certaines balises $allowedhtml = array( 'p' => array(), 'strong' => array(), 'em' => array(), 'a' => array( 'href' => array(), 'title' => array(), 'target' => array(), ), ); echo wpkses($usercontent, $allowedhtml); // Affiche :

Texte alert("XSS") gras

} / wpksespost() : Pour contenu de post / public static function examplewpksespost() { $usercontent = '

Texte

'; // Autorise les balises autorisées dans les posts echo wpksespost($usercontent); } /
esctextarea() : Pour textarea / public static function exampleesctextarea() { $userinput = 'textarea($userinput); echo ''; } / sanitizetextfield() : Nettoie le texte / public static function examplesanitize() { $userinput = "nNouvelle ligne"; // Retire les balises et normalise $clean = sanitizetextfield($userinput); // Résultat : "alert('XSS')Nouvelle ligne" return $clean; } } / Exemple complet : Formulaire de commentaire sécurisé / function yawcrendersecurecommentform($postid) { $currentuser = wpgetcurrentuser(); ?>
noncefield('submitcomment' . $postid, 'commentnonce'); ?>
Traitement sécurisé du formulaire / addaction('adminpostsubmitcomment', 'yawchandlecommentsubmission'); addaction('adminpostnoprivsubmitcomment', 'yawchandlecommentsubmission'); function yawchandlecommentsubmission() { // 1. Vérifier le nonce $postid = isset($POST['postid']) ? absint($POST['postid']) : 0; if (!isset($POST['commentnonce']) || !wpverifynonce($POST['commentnonce'], 'submitcomment' . $postid)) { wpdie('Erreur de sécurité : nonce invalide'); } // 2. Valider les données $errors = array(); $commentauthor = isset($POST['commentauthor']) ? sanitizetextfield($POST['commentauthor']) : ''; if (empty($commentauthor)) { $errors[] = 'Le nom est requis'; } $commentemail = isset($POST['commentemail']) ? sanitizeemail($POST['commentemail']) : ''; if (empty($commentemail) || !isemail($commentemail)) { $errors[] = 'Une adresse email valide est requise'; } $commenturl = isset($POST['commenturl']) ? escurlraw($POST['commenturl']) : ''; $commentcontent = isset($POST['commentcontent']) ? sanitizetextareafield($POST['commentcontent']) : ''; if (empty($commentcontent)) { $errors[] = 'Le commentaire ne peut pas être vide'; } // 3. Vérifier les erreurs if (!empty($errors)) { wpdie(implode('
', $errors)); } // 4. Insérer le commentaire $commentdata = array( 'commentpostID' => $postid, 'commentauthor' => $commentauthor, 'commentauthoremail' => $commentemail, 'commentauthorurl' => $commenturl, 'commentcontent' => $commentcontent, 'commenttype' => 'comment', 'commentparent' => 0, 'userid' => getcurrentuserid(), 'commentauthorIP' => YAWCSecurityFramework::getclientip(), 'commentagent' => isset($SERVER['HTTPUSERAGENT']) ? substr($SERVER['HTTPUSERAGENT'], 0, 254) : '', 'commentdate' => currenttime('mysql'), 'commentapproved' => 1, // Ou 0 pour modération ); $commentid = wpinsertcomment($commentdata); if ($commentid) { // Redirection vers l'article avec ancre vers le commentaire wpredirect(getpermalink($postid) . '#comment-' . $commentid); exit; } else { wpdie('Erreur lors de l'ajout du commentaire'); } }

Protection contre XSS dans JSON et AJAX


  Sécuriser les réponses AJAX
 /
addaction('wpajaxgetuserdata', 'yawcajaxgetuserdata');

function yawcajaxgetuserdata() {
    // Vérifier le nonce
    checkajaxreferer('get-user-data', 'nonce');

    $userid = isset($POST['userid']) ? absint($POST['userid']) : 0;

    if (!$userid) {
        wpsendjsonerror(array('message' => 'ID utilisateur invalide'));
    }

    $user = getuserdata($userid);

    if (!$user) {
        wpsendjsonerror(array('message' => 'Utilisateur non trouvé'));
    }

    // Nettoyer toutes les données avant de les envoyer
    $userdata = array(
        'id'           => absint($user->ID),
        'displayname' => sanitizetextfield($user->displayname),
        'email'        => sanitizeemail($user->useremail),
        'bio'          => wpksespost($user->description),
        'avatarurl'   => escurl(getavatarurl($user->ID)),
    );

    // wpsendjsonsuccess encode automatiquement en JSON
    wpsendjsonsuccess($userdata);
}

/
  JavaScript côté client
 /
?>
userdata',
                userid: $('#user-id').val(),
                nonce: 'createnonce('get-user-data'); ?>'
            },
            success: function(response) {
                if (response.success) {
                    var user = response.data;

                    // Échapper les données même en JS
                    var html = '
'; html += '### ' + $('
').text(user.displayname).html() + ''; html += '

' + $('

').text(user.email).html() + ''; html += '
' + user.bio + '
'; // Déjà nettoyé par wpksespost html += 'Avatar'; html += '
'; $('#user-container').html(html); } } }); }); });

4. Cross-Site Request Forgery (CSRF)

Comprendre CSRF

Le CSRF exploite la confiance qu'un site a envers un utilisateur authentifié en forçant l'exécution d'actions non autorisées.


 
VULNÉRABLE : Action sans vérification CSRF / function yawcdeletepostvulnerable() { $postid = $GET['postid']; // DANGER : N'importe qui peut créer un lien malveillant // wpdeletepost($postid, true); wpredirect(adminurl('edit.php')); exit; } / SÉCURISÉ : Utilisation de nonces / function yawcdeletepostsecure() { // Vérifier le nonce if (!isset($GET['wpnonce']) || !wpverifynonce($GET['wpnonce'], 'delete-post' . $GET['postid'])) { wpdie('Action non autorisée'); } // Vérifier les permissions if (!currentusercan('deletepost', $GET['postid'])) { wpdie('Permissions insuffisantes'); } $postid = absint($GET['postid']); wpdeletepost($postid, true); wpredirect(adminurl('edit.php')); exit; } / Génération du lien sécurisé / function yawcgetdeletepostlink($postid) { $url = adminurl('admin.php'); $url = addqueryarg(array( 'action' => 'deletepost', 'postid' => $postid, ), $url); // Ajouter le nonce $url = wpnonceurl($url, 'delete-post' . $postid); return $url; } // Affichage dans un template echo 'Supprimer';

Nonces pour formulaires


  Formulaire protégé par nonce
 /
function yawcrendersettingsform() {
    ?>
    
noncefield('yawcsavesettings', 'yawcsettingsnonce'); ?>
Traitement sécurisé / addaction('adminpostyawcsavesettings', 'yawcsavesettings'); function yawcsavesettings() { // Vérifier le nonce if (!isset($POST['yawcsettingsnonce']) || !wpverifynonce($POST['yawcsettingsnonce'], 'yawcsavesettings')) { wpdie('Nonce invalide'); } // Vérifier les permissions if (!currentusercan('manageoptions')) { wpdie('Permissions insuffisantes'); } // Traiter les données if (isset($POST['option1'])) { updateoption('yawcoption1', sanitizetextfield($POST['option1'])); } // Redirection wpredirect(addqueryarg('settings-updated', 'true', wpgetreferer())); exit; }

Nonces pour AJAX


  Configuration du nonce pour AJAX
 /
function yawcenqueueajaxscripts() {
    wpenqueuescript('yawc-ajax', gettemplatedirectoryuri() . '/js/ajax.js', array('jquery'), '1.0', true);

    wplocalizescript('yawc-ajax', 'yawcAjax', array(
        'ajaxurl' => adminurl('admin-ajax.php'),
        'nonce'   => wpcreatenonce('yawc-ajax-nonce'),
    ));
}
addaction('wpenqueuescripts', 'yawcenqueueajaxscripts');

/
  Handler AJAX sécurisé
 /
addaction('wpajaxyawcupdateprofile', 'yawcajaxupdateprofile');

function yawcajaxupdateprofile() {
    // Vérifier le nonce
    checkajaxreferer('yawc-ajax-nonce', 'nonce');

    // Vérifier l'authentification
    if (!isuserloggedin()) {
        wpsendjsonerror(array('message' => 'Vous devez être connecté'));
    }

    $userid = getcurrentuserid();

    // Valider et nettoyer les données
    $displayname = isset($POST['displayname'])
        ? sanitizetextfield($POST['displayname'])
        : '';

    if (empty($displayname)) {
        wpsendjsonerror(array('message' => 'Le nom est requis'));
    }

    // Mettre à jour
    $result = wpupdateuser(array(
        'ID'           => $userid,
        'displayname' => $displayname,
    ));

    if (iswperror($result)) {
        wpsendjsonerror(array('message' => $result->geterrormessage()));
    }

    wpsendjsonsuccess(array(
        'message' => 'Profil mis à jour',
        'user'    => array(
            'displayname' => $displayname,
        ),
    ));
}

JavaScript correspondant:

// ajax.js
jQuery(document).ready(function($) {
    $('#update-profile-form').on('submit', function(e) {
        e.preventDefault();

        $.ajax({
            url: yawcAjax.ajaxurl,
            type: 'POST',
            data: {
                action: 'yawcupdateprofile',
                nonce: yawcAjax.nonce,
                displayname: $('#displayname').val()
            },
            success: function(response) {
                if (response.success) {
                    alert(response.data.message);
                } else {
                    alert('Erreur : ' + response.data.message);
                }
            },
            error: function() {
                alert('Erreur de connexion');
            }
        });
    });
});

Gestion avancée des nonces


  Classe de gestion avancée des nonces
 /
class YAWCNonceManager {

    /
      Durée de vie des nonces (en secondes)
     /
    const NONCELIFETIME = 12  HOURINSECONDS;

    /
      Créer un nonce avec métadonnées
     /
    public static function create($action, $context = array()) {
        $nonce = wpcreatenonce($action);

        // Stocker les métadonnées du nonce
        $noncedata = array(
            'action'     => $action,
            'context'    => $context,
            'createdat' => currenttime('timestamp'),
            'userid'    => getcurrentuserid(),
            'ip'         => YAWCSecurityFramework::getclientip(),
        );

        settransient('yawcnonce' . $nonce, $noncedata, self::NONCELIFETIME);

        return $nonce;
    }

    /
      Vérifier un nonce avec validation stricte
     /
    public static function verify($nonce, $action, $strict = false) {
        // Vérification WordPress standard
        $verify = wpverifynonce($nonce, $action);

        if (!$verify) {
            self::logfailednonce($nonce, $action);
            return false;
        }

        if (!$strict) {
            return true;
        }

        // Vérifications supplémentaires en mode strict
        $noncedata = gettransient('yawcnonce' . $nonce);

        if (!$noncedata) {
            return true; // Les métadonnées sont optionnelles
        }

        // Vérifier que l'IP n'a pas changé
        if ($noncedata['ip'] !== YAWCSecurityFramework::getclientip()) {
            self::logfailednonce($nonce, $action, 'IP mismatch');
            return false;
        }

        // Vérifier que l'utilisateur n'a pas changé
        if ($noncedata['userid'] != getcurrentuserid()) {
            self::logfailednonce($nonce, $action, 'User ID mismatch');
            return false;
        }

        return true;
    }

    /
      Créer un nonce à usage unique
     /
    public static function createsingleuse($action) {
        $nonce = self::create($action, array('singleuse' => true));

        return $nonce;
    }

    /
      Vérifier et consommer un nonce à usage unique
     /
    public static function verifysingleuse($nonce, $action) {
        $noncedata = gettransient('yawcnonce' . $nonce);

        if (!$noncedata || empty($noncedata['context']['singleuse'])) {
            return self::verify($nonce, $action);
        }

        // Vérifier
        $valid = self::verify($nonce, $action);

        if ($valid) {
            // Consommer le nonce (le supprimer)
            deletetransient('yawcnonce' . $nonce);
        }

        return $valid;
    }

    /
      Logger les échecs de vérification
     /
    private static function logfailednonce($nonce, $action, $reason = 'Invalid nonce') {
        errorlog(sprintf(
            '[SECURITY] Failed nonce verification - Action: %s, Reason: %s, IP: %s, User: %d',
            $action,
            $reason,
            YAWCSecurityFramework::getclientip(),
            getcurrentuserid()
        ));

        // Incrémenter le compteur d'échecs
        $ip = YAWCSecurityFramework::getclientip();
        $key = 'noncefailures' . $ip;
        $failures = gettransient($key) ?: 0;
        $failures++;

        settransient($key, $failures, HOURINSECONDS);

        // Bloquer après 10 échecs
        if ($failures >= 10) {
            YAWCSecurityFramework::blockip($ip, 'Too many failed nonce verifications');
        }
    }

    /
      Nettoyer les vieux nonces
     /
    public static function cleanupexpired() {
        global $wpdb;

        $prefix = 'yawcnonce';

        $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$wpdb->options}
                 WHERE optionname LIKE %s
                 AND optionname LIKE '%%transient%%'",
                '%' . $wpdb->esclike($prefix) . '%'
            )
        );
    }
}

// Nettoyer les vieux nonces quotidiennement
if (!wpnextscheduled('yawccleanupnonces')) {
    wpscheduleevent(time(), 'daily', 'yawccleanupnonces');
}
addaction('yawccleanupnonces', array('YAWCNonceManager', 'cleanup_expired'));

Conclusion

La sécurité WordPress est un processus continu qui nécessite vigilance et mise à jour constante des pratiques. En suivant les principes présentés dans ce guide, vous construisez une défense solide contre les attaques courantes.

Points clés à retenir

  • Défense en profondeur - Multiplier les couches de sécurité
  • Validation des entrées - Ne jamais faire confiance aux données utilisateur
  • Échappement des sorties - Toujours échapper avant l'affichage
  • Requêtes préparées - Utiliser $wpdb->prepare() systématiquement
  • Nonces partout - Protéger toutes les actions avec des nonces
  • Principe du moindre privilège - Limiter les permissions au strict nécessaire
  • Logging et monitoring - Surveiller les activités suspectes
  • Mise à jour régulière - WordPress, plugins et thèmes à jour
  • Liste de vérification sécurité

  • [ ] Toutes les requêtes SQL utilisent prepare()
  • [ ] Toutes les sorties sont échappées avec les bonnes fonctions
  • [ ] Tous les formulaires incluent des nonces
  • [ ] Toutes les actions AJAX vérifient les nonces
  • [ ] Les uploads de fichiers sont validés et sécurisés
  • [ ] Les permissions sont vérifiées sur les actions sensibles
  • [ ] Les en-têtes de sécurité sont configurés
  • [ ] Les tentatives de connexion sont limitées
  • [ ] Les erreurs ne révèlent pas d'informations sensibles
  • [ ] Le logging de sécurité est en place
  • La sécurité n'est jamais terminée - restez informé des nouvelles vulnérabilités et mettez à jour vos pratiques régulièrement.

    Une remarque, un retour ?

    Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.