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
'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
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
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
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.
';
echo '' . $comment->comment_content . '
';
echo '