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
<?php
/**
* Stratégie de sécurité multi-couches
*/
class YAWC_Security_Framework {
/**
* 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::setup_server_security();
// Couche 2: Sécurité application
self::setup_application_security();
// Couche 3: Authentification
self::setup_authentication_security();
// Couche 4: Autorisation
self::setup_authorization_security();
// Couche 5: Validation & Sanitization
self::setup_data_security();
// Couche 6: Logging & Monitoring
self::setup_monitoring();
}
private static function setup_server_security() {
// Headers de sécurité
add_action('send_headers', array(__CLASS__, 'add_security_headers'));
// Désactiver l'affichage des erreurs en production
if (!WP_DEBUG) {
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
}
}
public static function add_security_headers() {
if (!is_admin()) {
// 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 setup_application_security() {
// Désactiver l'édition de fichiers
if (!defined('DISALLOW_FILE_EDIT')) {
define('DISALLOW_FILE_EDIT', true);
}
// Limiter les révisions
if (!defined('WP_POST_REVISIONS')) {
define('WP_POST_REVISIONS', 5);
}
// Désactiver le XML-RPC si non utilisé
add_filter('xmlrpc_enabled', '**return_false');
// Retirer les informations de version
remove_action('wp_head', 'wp_generator');
}
private static function setup_authentication_security() {
// Limiter les tentatives de connexion
add_action('wp_login_failed', array(__CLASS__, 'log_failed_login'));
// Forcer les mots de passe forts
add_action('user_profile_update_errors', array(__CLASS__, 'validate_password_strength'), 0, 3);
// Expirer les sessions inactives
add_filter('auth_cookie_expiration', array(__CLASS__, 'set_session_timeout'), 10, 3);
}
private static function setup_authorization_security() {
// Vérifier les capacités sur les actions sensibles
add_action('admin_init', array(**CLASS__, 'check_admin_capabilities'));
}
private static function setup_data_security() {
// Validation et sanitization seront détaillées dans les sections suivantes
}
private static function setup_monitoring() {
// Logging sera détaillé dans la section 10
}
public static function log_failed_login($username) {
$ip = self::get_client_ip();
error_log(sprintf(
'[SECURITY] Failed login attempt for user "%s" from IP %s at %s',
$username,
$ip,
current_time('mysql')
));
// Incrémenter le compteur de tentatives
$attempts_key = 'login_attempts*' . $ip;
$attempts = get_transient($attempts_key) ?: 0;
$attempts++;
set_transient($attempts_key, $attempts, HOUR_IN_SECONDS);
// Bloquer après 5 tentatives
if ($attempts >= 5) {
self::block_ip($ip, 'Too many failed login attempts');
wp_die('Trop de tentatives de connexion échouées. Votre IP a été temporairement bloquée.');
}
}
public static function validate_password_strength($errors, $update, $user) {
if (empty($*POST['pass1'])) {
return;
}
$password = $*POST['pass1'];
// Critères de mot de passe fort
$strength_requirements = array(
'length' => strlen($password) >= 12,
'uppercase' => preg_match('/[A-Z]/', $password),
'lowercase' => preg_match('/[a-z]/', $password),
'number' => preg_match('/[0-9]/', $password),
'special' => preg_match('/[^A-Za-z0-9]/', $password),
);
$failed_requirements = array();
if (!$strength_requirements['length']) {
$failed_requirements[] = 'au moins 12 caractères';
}
if (!$strength_requirements['uppercase']) {
$failed_requirements[] = 'une majuscule';
}
if (!$strength_requirements['lowercase']) {
$failed_requirements[] = 'une minuscule';
}
if (!$strength_requirements['number']) {
$failed_requirements[] = 'un chiffre';
}
if (!$strength_requirements['special']) {
$failed_requirements[] = 'un caractère spécial';
}
if (!empty($failed_requirements)) {
$errors->add(
'weak_password',
sprintf(
'Le mot de passe doit contenir %s.',
implode(', ', $failed_requirements)
)
);
}
// Vérifier contre les mots de passe communs
if (self::is_common_password($password)) {
$errors->add('common_password', 'Ce mot de passe est trop commun. Veuillez en choisir un autre.');
}
}
private static function is_common_password($password) {
$common_passwords = array(
'password', '12345678', 'qwerty', 'abc123', 'password123',
'admin', 'letmein', 'welcome', 'monkey', '1234567890',
);
return in_array(strtolower($password), $common_passwords);
}
public static function set_session_timeout($expiration, $user_id, $remember) {
// 2 heures si "se souvenir de moi" n'est pas coché
return $remember ? $expiration : 2 * HOUR_IN_SECONDS;
}
public static function get_client_ip() {
$ip_keys = array(
'HTTP_CLIENT_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
);
foreach ($ip_keys as $key) {
if (array_key_exists($key, $*SERVER) === true) {
foreach (explode(',', $*SERVER[$key]) as $ip) {
$ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
return $ip;
}
}
}
}
return isset($*SERVER['REMOTE_ADDR']) ? $*SERVER['REMOTE_ADDR'] : '0.0.0.0';
}
private static function block_ip($ip, $reason) {
// Stocker l'IP bloquée
$blocked_ips = get_option('yawc_blocked_ips', array());
$blocked_ips[$ip] = array(
'reason' => $reason,
'timestamp' => current_time('timestamp'),
'expires' => current_time('timestamp') + HOUR_IN_SECONDS,
);
update_option('yawc_blocked_ips', $blocked_ips);
}
public static function check_admin_capabilities() {
// Vérifier que l'utilisateur a bien les droits pour les actions sensibles
if (isset($*GET['action']) && $*GET['action'] === 'delete') {
if (!current_user_can('delete_posts')) {
wp_die('Vous n'avez pas les permissions nécessaires.');
}
}
}
}
// Initialiser le framework
add_action('init', array('YAWC_Security_Framework', 'init'));
2. Injection SQL et sécurisation des requêtes
Comprendre les injections SQL
<?php
/**
* VULNÉRABLE : N'UTILISEZ JAMAIS CE CODE
*/
function yawc_get_user_by_email_vulnerable($email) {
global $wpdb;
// DANGER : Injection SQL possible
$query = "SELECT * FROM {$wpdb->users} WHERE user_email = '$email'";
$user = $wpdb->get_row($query);
return $user;
}
// Attaque possible :
// yawc_get_user_by_email_vulnerable("' OR '1'='1' --");
// Résultat : Récupère tous les utilisateurs !
/**
* SÉCURISÉ : Toujours utiliser prepare()
*/
function yawc_get_user_by_email_secure($email) {
global $wpdb;
// Utiliser prepare() pour échapper automatiquement
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE user_email = %s",
$email
);
$user = $wpdb->get_row($query);
return $user;
}
Requêtes préparées avancées
<?php
/**
* Exemples de requêtes sécurisées
*/
class YAWC_Secure_Database {
/**
* Recherche sécurisée avec LIKE
*/
public static function search_posts($search_term) {
global $wpdb;
// Échapper les caractères spéciaux LIKE
$search_term = '%' . $wpdb->esc_like($search_term) . '%';
$query = $wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_title LIKE %s
AND post_status = 'publish'
AND post_type = 'post'
ORDER BY post_date DESC
LIMIT 20",
$search_term
);
return $wpdb->get_results($query);
}
/**
* Requête avec plusieurs paramètres
*/
public static function get_posts_by_author_and_category($author_id, $category_id, $limit = 10) {
global $wpdb;
$query = $wpdb->prepare(
"SELECT p.* FROM {$wpdb->posts} p
INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
WHERE p.post_author = %d
AND tt.term_id = %d
AND p.post_status = 'publish'
AND p.post_type = 'post'
ORDER BY p.post_date DESC
LIMIT %d",
absint($author_id),
absint($category_id),
absint($limit)
);
return $wpdb->get_results($query);
}
/**
* Insertion sécurisée
*/
public static function insert_custom_data($user_id, $data_type, $data_value) {
global $wpdb;
$table_name = $wpdb->prefix . 'custom_data';
// Utiliser wpdb->insert() pour l'échappement automatique
$result = $wpdb->insert(
$table_name,
array(
'user_id' => absint($user_id),
'data_type' => sanitize_key($data_type),
'data_value' => sanitize_text_field($data_value),
'created_at' => current_time('mysql'),
),
array('%d', '%s', '%s', '%s') // Types de données
);
if ($result === false) {
error_log('Database insert failed: ' . $wpdb->last_error);
return false;
}
return $wpdb->insert_id;
}
/**
* Mise à jour sécurisée
*/
public static function update_custom_data($id, $new_value) {
global $wpdb;
$table_name = $wpdb->prefix . 'custom_data';
$result = $wpdb->update(
$table_name,
array(
'data_value' => sanitize_text_field($new_value),
'updated_at' => current_time('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 delete_custom_data($id) {
global $wpdb;
$table_name = $wpdb->prefix . 'custom_data';
// Vérifier que l'utilisateur a le droit de supprimer
if (!current_user_can('delete_posts')) {
return false;
}
$result = $wpdb->delete(
$table_name,
array('id' => absint($id)),
array('%d')
);
return $result !== false;
}
/**
* Requête avec IN clause
*/
public static function get_posts_by_ids($post_ids) {
global $wpdb;
// Nettoyer et valider les IDs
$post_ids = array_map('absint', (array) $post_ids);
$post_ids = array_filter($post_ids); // Retirer les 0
if (empty($post_ids)) {
return array();
}
// Créer les placeholders
$placeholders = implode(',', array_fill(0, count($post_ids), '%d'));
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE ID IN ($placeholders)
AND post_status = 'publish'",
$post_ids
);
return $wpdb->get_results($query);
}
/**
* Transaction sécurisée
*/
public static function transfer_post_ownership($post_id, $from_user_id, $to_user_id) {
global $wpdb;
// Vérifier les permissions
if (!current_user_can('edit_others_posts')) {
return new WP_Error('permission_denied', 'Permission refusée');
}
// Démarrer la transaction
$wpdb->query('START TRANSACTION');
try {
// Mettre à jour le post
$result1 = $wpdb->update(
$wpdb->posts,
array('post_author' => absint($to_user_id)),
array(
'ID' => absint($post_id),
'post_author' => absint($from_user_id),
),
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 . 'ownership_log',
array(
'post_id' => absint($post_id),
'from_user_id' => absint($from_user_id),
'to_user_id' => absint($to_user_id),
'changed_at' => current_time('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');
error_log('Transaction failed: ' . $e->getMessage());
return new WP_Error('transaction_failed', $e->getMessage());
}
}
}
Validation et sanitization des entrées SQL
<?php
/**
* Fonctions de validation pour les requêtes
*/
class YAWC_SQL_Validator {
/**
* Valider un ORDER BY
*/
public static function validate_orderby($orderby, $allowed_columns) {
$orderby = sanitize_sql_orderby($orderby);
if (empty($orderby)) {
return 'post_date';
}
// Extraire la colonne
$orderby_parts = explode(' ', $orderby);
$column = $orderby_parts[0];
// Vérifier contre la liste blanche
if (!in_array($column, $allowed_columns, true)) {
return 'post_date';
}
return $orderby;
}
/**
* Valider une direction de tri
*/
public static function validate_order($order) {
$order = strtoupper($order);
return in_array($order, array('ASC', 'DESC'), true) ? $order : 'DESC';
}
/**
* Créer une requête de recherche sécurisée
*/
public static function build_search_query($search_term, $search_fields) {
global $wpdb;
if (empty($search_term)) {
return '';
}
$search_term = '%' . $wpdb->esc_like($search_term) . '%';
$search_conditions = array();
foreach ($search_fields as $field) {
$search_conditions[] = $wpdb->prepare(
"$field LIKE %s",
$search_term
);
}
return '(' . implode(' OR ', $search_conditions) . ')';
}
}
/**
* Utilisation
*/
function yawc_advanced_post_search($params) {
global $wpdb;
$defaults = array(
'search' => '',
'orderby' => 'post_date',
'order' => 'DESC',
'limit' => 20,
'offset' => 0,
);
$params = wp_parse_args($params, $defaults);
// Valider les paramètres
$allowed_orderby = array('post_date', 'post_title', 'comment_count', 'ID');
$orderby = YAWC_SQL_Validator::validate_orderby($params['orderby'], $allowed_orderby);
$order = YAWC_SQL_Validator::validate_order($params['order']);
$limit = absint($params['limit']);
$offset = absint($params['offset']);
// Construire la condition de recherche
$search_fields = array('post_title', 'post_content', 'post_excerpt');
$search_condition = YAWC_SQL_Validator::build_search_query($params['search'], $search_fields);
// Construire la requête finale
$where_clause = "post_status = 'publish' AND post_type = 'post'";
if (!empty($search_condition)) {
$where_clause .= " AND $search_condition";
}
$query = "SELECT * FROM {$wpdb->posts}
WHERE $where_clause
ORDER BY $orderby $order
LIMIT $limit OFFSET $offset";
return $wpdb->get_results($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.
<?php
/**
* VULNÉRABLE : N'utilisez jamais ce code
*/
function yawc_display_comment_vulnerable($comment) {
// DANGER : Le contenu du commentaire est affiché sans échappement
echo '<div class="comment">';
echo '<p>' . $comment->comment_content . '</p>';
echo '</div>';
}
// Si un attaquant poste : <script>alert('XSS')</script>
// Le script s'exécutera dans le navigateur de chaque visiteur !
/**
* SÉCURISÉ : Toujours échapper les sorties
*/
function yawc_display_comment_secure($comment) {
echo '<div class="comment">';
echo '<p>' . esc_html($comment->comment_content) . '</p>';
echo '</div>';
}
Fonctions d’échappement WordPress
<?php
/**
* Guide complet des fonctions d'échappement
*/
class YAWC_Escaping_Guide {
/**
* esc_html() : Pour le texte dans HTML
*/
public static function example_esc_html() {
$user_input = '<script>alert("XSS")</script>';
// Convertit les caractères spéciaux en entités HTML
echo '<p>' . esc_html($user_input) . '</p>';
// Affiche : <script>alert("XSS")</script>
}
/**
* esc_attr() : Pour les attributs HTML
*/
public static function example_esc_attr() {
$user_input = '" onclick="alert('XSS')';
// Sécurise les guillemets et caractères spéciaux
echo '<input type="text" value="' . esc_attr($user_input) . '">';
}
/**
* esc_url() : Pour les URLs
*/
public static function example_esc_url() {
$user_url = 'javascript:alert("XSS")';
// Valide et nettoie l'URL
echo '<a href="' . esc_url($user_url) . '">Lien</a>';
// Le javascript: sera retiré
}
/**
* esc_js() : Pour JavaScript
*/
public static function example_esc_js() {
$user_input = '"; alert("XSS"); var x="';
echo '<script>';
echo 'var message = "' . esc_js($user_input) . '";';
echo '</script>';
}
/**
* wp_kses() : Pour HTML autorisé
*/
public static function example_wp_kses() {
$user_content = '<p>Texte <script>alert("XSS")</script> __gras__</p>';
// Autoriser seulement certaines balises
$allowed_html = array(
'p' => array(),
'strong' => array(),
'em' => array(),
'a' => array(
'href' => array(),
'title' => array(),
'target' => array(),
),
);
echo wp_kses($user_content, $allowed_html);
// Affiche : <p>Texte alert("XSS") __gras__</p>
}
/**
* wp_kses_post() : Pour contenu de post
*/
public static function example_wp_kses_post() {
$user_content = '<p>Texte <script>alert("XSS")</script></p>';
// Autorise les balises autorisées dans les posts
echo wp_kses_post($user_content);
}
/**
* esc_textarea() : Pour textarea
*/
public static function example_esc_textarea() {
$user_input = '</textarea><script>alert("XSS")</script><textarea>';
echo '<textarea>';
echo esc_textarea($user_input);
echo '</textarea>';
}
/**
* sanitize_text_field() : Nettoie le texte
*/
public static function example_sanitize() {
$user_input = "<script>alert('XSS')</script>nNouvelle ligne";
// Retire les balises et normalise
$clean = sanitize_text_field($user_input);
// Résultat : "alert('XSS')Nouvelle ligne"
return $clean;
}
}
/**
* Exemple complet : Formulaire de commentaire sécurisé
*/
function yawc_render_secure_comment_form($post_id) {
$current_user = wp_get_current_user();
?>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="comment-form">
<?php wp_nonce_field('submit_comment*' . $post_id, 'comment_nonce'); ?>
<input type="hidden" name="action" value="submit_comment">
<input type="hidden" name="post_id" value="<?php echo absint($post_id); ?>">
<div class="form-field">
<label for="comment_author">
Nom <span class="required">*</span>
</label>
<input
type="text"
name="comment_author"
id="comment_author"
value="<?php echo esc_attr($current_user->display_name); ?>"
required
>
</div>
<div class="form-field">
<label for="comment_email">
Email <span class="required">*</span>
</label>
<input
type="email"
name="comment_email"
id="comment_email"
value="<?php echo esc_attr($current_user->user_email); ?>"
required
>
</div>
<div class="form-field">
<label for="comment_url">Site web</label>
<input
type="url"
name="comment_url"
id="comment_url"
value="<?php echo esc_url($current_user->user_url); ?>"
>
</div>
<div class="form-field">
<label for="comment_content">
Commentaire <span class="required">*</span>
</label>
<textarea
name="comment_content"
id="comment_content"
rows="5"
required
><?php echo isset($*POST['comment_content']) ? esc_textarea($*POST['comment_content']) : ''; ?></textarea>
</div>
<button type="submit" class="submit-button">
Publier le commentaire
</button>
</form>
<?php
}
/**
* Traitement sécurisé du formulaire
*/
add_action('admin_post_submit_comment', 'yawc_handle_comment_submission');
add_action('admin_post_nopriv_submit_comment', 'yawc_handle_comment_submission');
function yawc_handle_comment_submission() {
// 1. Vérifier le nonce
$post_id = isset($*POST['post_id']) ? absint($*POST['post_id']) : 0;
if (!isset($*POST['comment_nonce']) ||
!wp_verify_nonce($*POST['comment_nonce'], 'submit_comment*' . $post_id)) {
wp_die('Erreur de sécurité : nonce invalide');
}
// 2. Valider les données
$errors = array();
$comment_author = isset($*POST['comment_author'])
? sanitize_text_field($*POST['comment_author'])
: '';
if (empty($comment_author)) {
$errors[] = 'Le nom est requis';
}
$comment_email = isset($*POST['comment_email'])
? sanitize_email($*POST['comment_email'])
: '';
if (empty($comment_email) || !is_email($comment_email)) {
$errors[] = 'Une adresse email valide est requise';
}
$comment_url = isset($*POST['comment_url'])
? esc_url_raw($*POST['comment_url'])
: '';
$comment_content = isset($*POST['comment_content'])
? sanitize_textarea_field($*POST['comment_content'])
: '';
if (empty($comment_content)) {
$errors[] = 'Le commentaire ne peut pas être vide';
}
// 3. Vérifier les erreurs
if (!empty($errors)) {
wp_die(implode('
', $errors));
}
// 4. Insérer le commentaire
$comment_data = array(
'comment_post_ID' => $post_id,
'comment_author' => $comment_author,
'comment_author_email' => $comment_email,
'comment_author_url' => $comment_url,
'comment_content' => $comment_content,
'comment_type' => 'comment',
'comment_parent' => 0,
'user_id' => get_current_user_id(),
'comment_author_IP' => YAWC_Security_Framework::get_client_ip(),
'comment_agent' => isset($*SERVER['HTTP_USER_AGENT'])
? substr($*SERVER['HTTP_USER_AGENT'], 0, 254)
: '',
'comment_date' => current_time('mysql'),
'comment_approved' => 1, // Ou 0 pour modération
);
$comment_id = wp_insert_comment($comment_data);
if ($comment_id) {
// Redirection vers l'article avec ancre vers le commentaire
wp_redirect(get_permalink($post_id) . '#comment-' . $comment_id);
exit;
} else {
wp_die('Erreur lors de l'ajout du commentaire');
}
}
Protection contre XSS dans JSON et AJAX
<?php
/**
* Sécuriser les réponses AJAX
*/
add_action('wp_ajax_get_user_data', 'yawc_ajax_get_user_data');
function yawc_ajax_get_user_data() {
// Vérifier le nonce
check_ajax_referer('get-user-data', 'nonce');
$user_id = isset($*POST['user_id']) ? absint($*POST['user_id']) : 0;
if (!$user_id) {
wp_send_json_error(array('message' => 'ID utilisateur invalide'));
}
$user = get_userdata($user_id);
if (!$user) {
wp_send_json_error(array('message' => 'Utilisateur non trouvé'));
}
// Nettoyer toutes les données avant de les envoyer
$user_data = array(
'id' => absint($user->ID),
'display_name' => sanitize_text_field($user->display_name),
'email' => sanitize_email($user->user_email),
'bio' => wp_kses_post($user->description),
'avatar_url' => esc_url(get_avatar_url($user->ID)),
);
// wp_send_json_success encode automatiquement en JSON
wp_send_json_success($user_data);
}
/**
* JavaScript côté client
*/
?>
<script>
jQuery(document).ready(function($) {
$('#load-user').on('click', function() {
$.ajax({
url: ajaxurl,
type: 'POST',
data: {
action: 'get_user_data',
user_id: $('#user-id').val(),
nonce: '<?php echo wp_create_nonce('get-user-data'); ?>'
},
success: function(response) {
if (response.success) {
var user = response.data;
// Échapper les données même en JS
var html = '<div class="user-card">';
html += '### ' + $('<div>').text(user.display_name).html() + '';
html += '<p>' + $('<div>').text(user.email).html() + '</p>';
html += '<div>' + user.bio + '</div>'; // Déjà nettoyé par wp_kses_post
html += '<img src="' + user.avatar_url + '" alt="Avatar">';
html += '</div>';
$('#user-container').html(html);
}
}
});
});
});
</script>
<?php
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.
<?php
/**
* VULNÉRABLE : Action sans vérification CSRF
*/
function yawc_delete_post_vulnerable() {
$post_id = $*GET['post_id'];
// DANGER : N'importe qui peut créer un lien malveillant
// <img src="https://votresite.com/wp-admin/admin.php?action=delete&post_id=123">
wp_delete_post($post_id, true);
wp_redirect(admin_url('edit.php'));
exit;
}
/**
* SÉCURISÉ : Utilisation de nonces
*/
function yawc_delete_post_secure() {
// Vérifier le nonce
if (!isset($*GET['*wpnonce']) ||
!wp_verify_nonce($*GET['*wpnonce'], 'delete-post*' . $*GET['post_id'])) {
wp_die('Action non autorisée');
}
// Vérifier les permissions
if (!current_user_can('delete_post', $*GET['post_id'])) {
wp_die('Permissions insuffisantes');
}
$post_id = absint($*GET['post_id']);
wp_delete_post($post_id, true);
wp_redirect(admin_url('edit.php'));
exit;
}
/**
* Génération du lien sécurisé
*/
function yawc_get_delete_post_link($post_id) {
$url = admin_url('admin.php');
$url = add_query_arg(array(
'action' => 'delete_post',
'post_id' => $post_id,
), $url);
// Ajouter le nonce
$url = wp_nonce_url($url, 'delete-post*' . $post_id);
return $url;
}
// Affichage dans un template
echo '<a href="' . esc_url(yawc_get_delete_post_link(123)) . '">Supprimer</a>';
Nonces pour formulaires
<?php
/**
* Formulaire protégé par nonce
*/
function yawc_render_settings_form() {
?>
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
<?php
// Générer un champ nonce caché
wp_nonce_field('yawc_save_settings', 'yawc_settings_nonce');
?>
<input type="hidden" name="action" value="yawc_save_settings">
<label>
Option 1:
<input type="text" name="option_1" value="<?php echo esc_attr(get_option('yawc_option_1')); ?>">
</label>
<button type="submit">Enregistrer</button>
</form>
<?php
}
/**
* Traitement sécurisé
*/
add_action('admin_post_yawc_save_settings', 'yawc_save_settings');
function yawc_save_settings() {
// Vérifier le nonce
if (!isset($*POST['yawc_settings_nonce']) ||
!wp_verify_nonce($*POST['yawc_settings_nonce'], 'yawc_save_settings')) {
wp_die('Nonce invalide');
}
// Vérifier les permissions
if (!current_user_can('manage_options')) {
wp_die('Permissions insuffisantes');
}
// Traiter les données
if (isset($*POST['option_1'])) {
update_option('yawc_option_1', sanitize_text_field($*POST['option_1']));
}
// Redirection
wp_redirect(add_query_arg('settings-updated', 'true', wp_get_referer()));
exit;
}
Nonces pour AJAX
<?php
/**
* Configuration du nonce pour AJAX
*/
function yawc_enqueue_ajax_scripts() {
wp_enqueue_script('yawc-ajax', get_template_directory_uri() . '/js/ajax.js', array('jquery'), '1.0', true);
wp_localize_script('yawc-ajax', 'yawcAjax', array(
'ajaxurl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('yawc-ajax-nonce'),
));
}
add_action('wp_enqueue_scripts', 'yawc_enqueue_ajax_scripts');
/**
* Handler AJAX sécurisé
*/
add_action('wp_ajax_yawc_update_profile', 'yawc_ajax_update_profile');
function yawc_ajax_update_profile() {
// Vérifier le nonce
check_ajax_referer('yawc-ajax-nonce', 'nonce');
// Vérifier l'authentification
if (!is_user_logged_in()) {
wp_send_json_error(array('message' => 'Vous devez être connecté'));
}
$user_id = get_current_user_id();
// Valider et nettoyer les données
$display_name = isset($*POST['display_name'])
? sanitize_text_field($*POST['display_name'])
: '';
if (empty($display_name)) {
wp_send_json_error(array('message' => 'Le nom est requis'));
}
// Mettre à jour
$result = wp_update_user(array(
'ID' => $user_id,
'display_name' => $display_name,
));
if (is_wp_error($result)) {
wp_send_json_error(array('message' => $result->get_error_message()));
}
wp_send_json_success(array(
'message' => 'Profil mis à jour',
'user' => array(
'display_name' => $display_name,
),
));
}
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: 'yawc_update_profile',
nonce: yawcAjax.nonce,
display_name: $('#display_name').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
<?php
/**
* Classe de gestion avancée des nonces
*/
class YAWC_Nonce_Manager {
/**
* Durée de vie des nonces (en secondes)
*/
const NONCE_LIFETIME = 12 * HOUR_IN_SECONDS;
/**
* Créer un nonce avec métadonnées
*/
public static function create($action, $context = array()) {
$nonce = wp_create_nonce($action);
// Stocker les métadonnées du nonce
$nonce_data = array(
'action' => $action,
'context' => $context,
'created_at' => current_time('timestamp'),
'user_id' => get_current_user_id(),
'ip' => YAWC_Security_Framework::get_client_ip(),
);
set_transient('yawc_nonce*' . $nonce, $nonce_data, self::NONCE_LIFETIME);
return $nonce;
}
/**
* Vérifier un nonce avec validation stricte
*/
public static function verify($nonce, $action, $strict = false) {
// Vérification WordPress standard
$verify = wp_verify_nonce($nonce, $action);
if (!$verify) {
self::log_failed_nonce($nonce, $action);
return false;
}
if (!$strict) {
return true;
}
// Vérifications supplémentaires en mode strict
$nonce_data = get_transient('yawc_nonce*' . $nonce);
if (!$nonce_data) {
return true; // Les métadonnées sont optionnelles
}
// Vérifier que l'IP n'a pas changé
if ($nonce_data['ip'] !== YAWC_Security_Framework::get_client_ip()) {
self::log_failed_nonce($nonce, $action, 'IP mismatch');
return false;
}
// Vérifier que l'utilisateur n'a pas changé
if ($nonce_data['user_id'] != get_current_user_id()) {
self::log_failed_nonce($nonce, $action, 'User ID mismatch');
return false;
}
return true;
}
/**
* Créer un nonce à usage unique
*/
public static function create_single_use($action) {
$nonce = self::create($action, array('single_use' => true));
return $nonce;
}
/**
* Vérifier et consommer un nonce à usage unique
*/
public static function verify_single_use($nonce, $action) {
$nonce_data = get_transient('yawc_nonce*' . $nonce);
if (!$nonce_data || empty($nonce_data['context']['single_use'])) {
return self::verify($nonce, $action);
}
// Vérifier
$valid = self::verify($nonce, $action);
if ($valid) {
// Consommer le nonce (le supprimer)
delete_transient('yawc_nonce*' . $nonce);
}
return $valid;
}
/**
* Logger les échecs de vérification
*/
private static function log_failed_nonce($nonce, $action, $reason = 'Invalid nonce') {
error_log(sprintf(
'[SECURITY] Failed nonce verification - Action: %s, Reason: %s, IP: %s, User: %d',
$action,
$reason,
YAWC_Security_Framework::get_client_ip(),
get_current_user_id()
));
// Incrémenter le compteur d'échecs
$ip = YAWC_Security_Framework::get_client_ip();
$key = 'nonce_failures*' . $ip;
$failures = get_transient($key) ?: 0;
$failures++;
set_transient($key, $failures, HOUR_IN_SECONDS);
// Bloquer après 10 échecs
if ($failures >= 10) {
YAWC_Security_Framework::block_ip($ip, 'Too many failed nonce verifications');
}
}
/**
* Nettoyer les vieux nonces
*/
public static function cleanup_expired() {
global $wpdb;
$prefix = 'yawc_nonce*';
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE %s
AND option_name LIKE '%%transient%%'",
'%' . $wpdb->esc_like($prefix) . '%'
)
);
}
}
// Nettoyer les vieux nonces quotidiennement
if (!wp_next_scheduled('yawc_cleanup_nonces')) {
wp_schedule_event(time(), 'daily', 'yawc_cleanup_nonces');
}
add_action('yawc_cleanup_nonces', array('YAWC_Nonce_Manager', '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
$wpdb->prepare() systématiquementListe de vérification sécurité
prepare()La sécurité n’est jamais terminée – restez informé des nouvelles vulnérabilités et mettez à jour vos pratiques régulièrement.