REST API : Design patterns et meilleures pratiques
Introduction Les API REST (Representational State Transfer) sont devenues le standard de facto pour la…
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.
┌─────────────────────────────────────────────────┐
│ 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% │
│ │
└─────────────────────────────────────────────────┘
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'));
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;
}
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());
}
}
}
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);
}
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 '' . eschtml($comment->commentcontent) . '
';
echo '';
}
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();
?>
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');
}
}
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 += '
';
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() {
?>
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.
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.
' . $comment->commentcontent . '
'; echo '