Protection WordPress : prévenir la faille SQL injection (approche WordPress)

Une injection SQL, ce n’est pas un concept abstrait réservé aux vieux écrans de terminal. Dans un site WordPress, elle naît presque toujours du même scénario: une entrée utilisateur finit par se retrouver dans une requête SQL sans traitement strict, ou avec un traitement incomplet, au point que l’attaquant peut modifier l’intention de la requête. Parfois, le résultat est “juste” une fuite de données. Parfois, c’est une altération silencieuse, des comptes créés, ou des formulaires qui semblent fonctionner normalement mais servent d’ouverture.

J’ai vu des sites où la vulnérabilité n’était pas dans le core WordPress. Elle venait d’un plugin, d’un thème sur mesure, ou d’un bout de code “rapide” ajouté pour un besoin ponctuel. L’approche WordPress aide beaucoup, parce qu’elle propose déjà des briques solides, notamment via la classe $wpdb. Mais cette solidité ne dispense pas d’une discipline stricte côté développement et côté exploitation. L’idée, c’est de réduire au maximum la surface d’entrée et de s’assurer que toute donnée qui traverse la frontière “web” vers “base de données” est traitée comme une donnée, pas comme du code.

Là où l’injection SQL se cache vraiment dans WordPress

WordPress expose énormément de points d’entrée. On pense tout de suite au formulaire de contact, à la page de recherche ou aux paramètres d’URL du type ?id=123. En pratique, l’injection SQL apparaît aussi dans des endroits plus “fonctionnels”: un shortcode qui lit une variable depuis des attributs, un endpoint AJAX (admin-ajax.php), un hook qui reçoit des données depuis une requête front, ou un traitement sur save_post qui enregistre un champ personnalisé.

Le mécanisme est souvent le même:

    l’application récupère une valeur via $_GET, $_POST, $_REQUEST ou une API comme get_param(), cette valeur n’est pas validée avec assez de rigueur, elle est ensuite concaténée dans une requête SQL, la requête est exécutée sans requêtes préparées et sans mécanisme de paramétrage.

La faille ne dépend pas uniquement de la présence de guillemets. Elle dépend de la manière dont le code construit la requête. Il suffit d’un seul chemin “non préparé” dans un plugin ou un thème pour ouvrir la porte.

Ce qui complique les audits, c’est que beaucoup de code “a l’air correct” parce qu’il utilise esc_sql() ou des fonctions d’échappement. Or, l’échappement n’est pas une garantie universelle. Sur WordPress, la bonne pratique orientée SQL reste l’usage des requêtes préparées, et plus précisément $wpdb->prepare() ou les méthodes qui en découlent.

Comprendre le rôle de $wpdb et des requêtes préparées

Dans WordPress, $wpdb est votre meilleur allié pour deux raisons. D’abord, il vous donne un cadre cohérent, avec une manière standard de formuler des requêtes. Ensuite, il facilite l’usage de mécanismes de paramétrage, ce qui réduit fortement le risque d’interprétation des entrées comme du SQL.

Une requête préparée, dans son principe, impose une séparation claire entre:

    le texte SQL (la structure de la requête), et les valeurs (les paramètres).

Au lieu de fabriquer une requête “complète” avec des bouts de chaîne, vous préparez une requête avec des placeholders, puis vous fournissez les valeurs en paramètres. Le moteur de base de données traite ces valeurs comme des données.

image

Voici un exemple de logique typique à rechercher dans vos plugins et thèmes. Le style vulnérable ressemble à une concaténation:

$sql = "SELECT * FROM wp_users WHERE ID = " . $_GET['id']; $rows = $wpdb->get_results($sql);

Même si l’on “croit” que id est numérique, ce code laisse la porte ouverte. À l’inverse, la logique robuste utilise les formats attendus et un mécanisme de préparation:

$id = isset($_GET['id']) ? (int) $_GET['id'] : 0; $sql = $wpdb->prepare("SELECT * FROM $wpdb->users WHERE ID = %d", $id); $rows = $wpdb->get_results($sql);

Le point important n’est pas seulement le (int). C’est la combinaison: conversion vers le type attendu, puis préparation avec le placeholder %d. Pour une chaîne, on utilise %s. Pour un booléen, WordPress le mappe souvent via un entier selon les contextes, et on choisit le placeholder adéquat.

Dans un code réel, on voit aussi des variantes où la requête est correcte, mais où les paramètres sont préparés trop tard, ou préparés partiellement. Le contrôle doit s’exercer sur tout le segment où des données se branchent dans la requête.

Les pièges fréquents, même chez les développeurs prudents

On pourrait croire que l’erreur est “simple” et qu’elle se voit immédiatement. En réalité, il existe des pièges qui donnent une fausse impression de sécurité.

Confondre échappement et préparation

WordPress dispose de fonctions utiles pour échapper des valeurs. Mais l’échappement n’est pas une préparation au sens SQL. Si votre code finit par assembler une requête avec des valeurs échappées mais intégrées dans la syntaxe, il reste possible de tomber sur un cas limite selon le type de requête et la manière dont le code concatène.

Je l’ai vu sur des requêtes dynamiques de tri, où une variable “order by” était incluse. Même avec une valeur “plausible” fournie par l’utilisateur, un cas mal géré peut casser l’intention du code.

Utiliser $_REQUEST par confort

Beaucoup de code “ancien” utilise $_REQUEST, parce que c’est pratique: ça mélange GET, POST et cookies. Or, ce mélange complique la validation, parce que vous perdez la trace du canal d’entrée. Pour une défense solide, vous voulez savoir d’où vient la donnée et imposer une validation adaptée. Et vous voulez éviter les surprises, par exemple quand un cookie a la même clé qu’un champ de formulaire.

Validation incomplète

Valider “un peu” ne suffit pas. Si un champ est supposé être un identifiant numérique, vous devez réellement garantir un type. Un is_numeric() peut être insuffisant si ensuite la valeur est traitée comme chaîne, ou si des espaces, des notations inattendues, ou des encodages atypiques passent. Pour les identifiants, une conversion explicite vers un type (entier, en l’occurrence) est souvent plus robuste.

Pour les champs texte, la validation doit aussi tenir compte du format attendu. Un email valide n’est pas seulement une chaîne échappée. Vous devez vérifier sa structure, sa longueur, et parfois ses contraintes applicatives.

“Sauts” dans le flux

Une source de vulnérabilité peut être introduite plus loin que l’endroit où la valeur est récupérée. Une valeur est d’abord validée, puis transformée, puis réinjectée dans une requête via une autre fonction. Si cette autre fonction reconstruit une requête en concaténant, la validation initiale peut devenir inopérante.

C’est pour cela que l’audit doit être traçable: remonter le chemin de donnée jusqu’au point d’exécution SQL.

Une méthode pratique d’audit WordPress, sans panique

L’audit n’a pas besoin d’être “théorique”. Sur un site WordPress, vous pouvez procéder par observation du comportement, et par inspection ciblée du code. Le risque, c’est de multiplier les recherches aléatoires. Le gain, c’est de suivre les signaux.

1) Identifier les points où la donnée “arrive”

Commencez par lister les entrées web réellement utilisées: formulaires, paramètres d’URL, paramètres d’ajax, shortcodes, métaboxes, champs personnalisés.

En pratique, la donnée passe souvent par $_GET et $_POST, ou via sanitize_text_field dans des lieux où c’est déjà un bon début. Mais attention: sanitize_text_field ne fait pas une validation SQL, et ne remplace pas la préparation.

2) Chercher les traces de requêtes SQL construites

Dans vos plugins et thèmes, inspectez les occurrences de:

    $wpdb->query(, $wpdb->get_results(, $wpdb->get_row(, $wpdb->get_var(, des chaînes SQL manipulées puis exécutées.

Repérez ensuite les cas où la chaîne SQL contient une concaténation (.) avec des variables. Les fonctions préparées sont votre repère positif, les concaténations répétées votre repère négatif.

3) Contrôler la cohérence jusqu’au bout

Une fois que vous avez repéré le point d’exécution, revenez en arrière et vérifiez:

image

    le type attendu de chaque paramètre, la conversion appliquée, la préparation appliquée.

Si vous constatez que la donnée est préparée avec un placeholder du mauvais type, vous réduisez la sécurité sans le vouloir. Par exemple, utiliser %s quand la requête exige https://gardewp.fr/securite-wordpress/ un entier peut ouvrir des cas inattendus selon le contexte. Ce n’est pas la panacée, mais l’intention doit être alignée.

Pour rendre le processus concret, voici une petite checklist de triage à garder sous la main lors d’un audit de code WordPress:

    vérifier toutes les requêtes SQL construites dans plugins et thèmes repérer les concaténations dans la chaîne SQL s’assurer que chaque valeur externe passe par $wpdb->prepare avec le bon placeholder contrôler les conversions explicites (entier, chaîne) avant la préparation éviter $_REQUEST et clarifier la source de chaque valeur

Corrections: comment “remplacer la concaténation par la préparation” dans le quotidien

Quand on corrige une injection SQL dans WordPress, on ne corrige pas seulement la ligne fautive. On corrige aussi la manière de traiter les données dans tout le chemin.

La plupart du temps, le correctif ressemble à ça:

    on récupère la valeur, on valide le type et le format attendu, on prépare la requête SQL avec des placeholders, on exécute, on gère les cas d’erreur sans exposer d’informations sensibles.

Prenons un exemple plus réaliste, un endpoint AJAX qui renvoie des résultats filtrés. Les filtres viennent typiquement du front via JavaScript.

Supposons qu’un plugin a une logique de recherche sur des enregistrements d’une table custom, avec un paramètre category et un paramètre limit.

Le code vulnérable a souvent deux défauts: il met un paramètre dans une clause SQL sans préparation, et il utilise LIMIT ou ORDER BY de façon dynamique avec des valeurs non contrôlées. Le correctif vise à:

    convertir limit en entier strictement borné (par exemple entre 1 et 100), valider category comme chaîne conforme à un ensemble attendu ou au moins comme texte avec longueur contrôlée, préparer la requête pour les valeurs.

Une attention particulière doit être portée à la clause ORDER BY. Même avec des placeholders, vous ne pouvez pas substituer un champ SQL arbitraire comme une simple valeur paramétrée. Le moyen sûr est de limiter l’option à une liste de colonnes autorisées côté serveur, puis de choisir la colonne en dur. On s’appuie alors sur le jugement, pas sur un “échappement magique”.

C’est un endroit où les développeurs hésitent. En pratique, la logique est simple: si l’utilisateur peut choisir le tri, vous devez traduire ce choix vers une colonne autorisée, pas vers une expression SQL.

La couche WordPress: sanitization, validation, droits, et contexte

La préparation SQL est la pièce maîtresse, mais elle ne vit pas seule. Dans WordPress, une défense réaliste est une combinaison de plusieurs couches.

image

Sanitization et validation ne remplacent pas la préparation, mais elles réduisent la surface

Sanitizer sert à diminuer les entrées problématiques, limiter la longueur, nettoyer des caractères inutiles. Valider sert à rejeter ce qui ne correspond pas au format attendu. Ces deux actions ne sont pas une garantie contre l’injection SQL à elles seules, mais elles limitent les chemins qui arrivent en base de données avec des valeurs inattendues.

Vérifier les droits avant d’interroger la base

Beaucoup de vulnérabilités ne visent pas seulement l’injection. Elles visent aussi l’accès à des données que l’utilisateur ne devrait pas voir. Même si la requête SQL est préparée correctement, un endpoint mal protégé peut permettre à un utilisateur non autorisé de demander des données qu’il ne devrait pas consulter.

Dans un environnement WordPress, l’accès passe souvent par des contrôles de capability (current_user_can) ou des vérifications d’accès avant de faire la requête. C’est particulièrement important pour les actions en AJAX ou sur des endpoints privés.

Utiliser des nonces pour les actions sensibles

Les nonces ne bloquent pas une injection SQL directement. Ils empêchent surtout l’exécution d’actions non autorisées ou le déclenchement par des tiers. Dans une défense globale, c’est utile, notamment pour des actions qui modifient des données. Côté lecture, l’impact est différent, mais pour la cohérence du modèle de sécurité, ça compte.

Exposition indirecte: quand une injection donne un résultat “qui semble normal”

Une injection SQL ne se traduit pas toujours par une page d’erreur. Beaucoup de fois, elle se manifeste par un comportement discret: un filtre qui retourne des résultats inattendus, une liste qui s’élargit, ou un identifiant qui semble “accidentellement” accepté.

Lors d’incidents, j’ai remarqué que les équipes se focalisent sur les erreurs visibles, alors que la base de données renvoie souvent des ensembles de résultats valides. Un code vulnérable peut donc rester non détecté pendant longtemps.

Deux signaux doivent alerter:

    des paramètres qui acceptent des valeurs absurdes sans rejet, des requêtes qui semblent fonctionner même quand la forme attendue est violée.

La corrélation entre journaux applicatifs et comportement front est souvent plus utile qu’une recherche purement “sur code”, surtout dans des plugins tiers où le code source peut être minime ou complexe.

Durcir aussi côté exploitation: limiter les dégâts si une faille existe

Même avec une correction parfaite, vous devez anticiper le “reste du monde”: plugins tiers, thèmes, mises à jour, erreurs humaines, et parfois des cas que personne n’a anticipés. Côté WordPress, vous pouvez réduire l’impact d’une injection SQL, même si la prévention primaire est ailleurs.

Je regroupe souvent ces mesures en trois catégories: réduction d’exposition, durcissement de la base, et surveillance.

    réduction d’exposition: limiter les endpoints accessibles publiquement, réduire les informations dans les messages d’erreur, éviter des pages qui exposent des paramètres sans contrôles, durcissement de la base: comptes MySQL avec permissions minimales, éviter que l’application puisse faire plus que nécessaire, surveillance: journaux, alertes sur patterns inhabituels, et revue périodique.

La question délicate est d’équilibrer sécurité et maintenance. Un durcissement trop agressif peut casser des plugins. L’objectif est de viser la cohérence: l’application doit avoir les droits strictement nécessaires, pas des droits “par confort” pour tout faire.

Construire une culture de dev sécurisée sur WordPress

Une faille d’injection SQL revient quand le code change. WordPress évolue via des mises à jour, des extensions, et des modifications internes. Si vous voulez éviter la répétition, vous avez besoin d’un processus.

Ce processus n’a pas besoin d’être lourd. Il doit juste être répété avec régularité et exigence. Par exemple, intégrer une revue de code centrée sur les requêtes SQL, et imposer comme règle que toute requête utilisant des données externes passe par $wpdb->prepare.

Si vous gérez plusieurs sites, ou si vous travaillez avec des prestataires, la règle doit être simple à appliquer et simple à auditer. Dans ma pratique, une mini checklist “avant merge” aide beaucoup:

    aucune requête SQL ne doit concaténer une donnée externe dans la chaîne SQL les valeurs doivent être validées et converties avant la préparation (type, bornes, longueur) les placeholders %d et %s doivent correspondre au type attendu les choix dynamiques de colonnes (tri, filtres) doivent être mappés depuis une liste blanche les endpoints sensibles doivent vérifier les droits et, si action, utiliser un nonce

C’est ce dernier point qui évite un scénario où l’injection n’est pas exploitée pour exfiltrer, mais pour contourner une logique applicative.

Cas particuliers: champs custom, requêtes complexes, et dynamiques “pour gagner du temps”

Les sites WordPress avancés utilisent des tables custom, des champs via ACF ou des métadonnées, des requêtes combinées, des jointures, des filtres multiples. Ici, le risque n’est pas seulement l’injection, c’est aussi la complexité.

Quand vous construisez une requête dynamique, le danger est de “fragmenter” le SQL: vous ajoutez une clause conditionnelle avec une concaténation de morceaux, puis vous oubliez de préparer la valeur qui entre dans ce morceau.

La bonne approche est de préparer séparément ce qui est variable en valeurs, et de garder la structure SQL contrôlée. Pour les clauses conditionnelles, vous pouvez choisir d’ajouter des segments entiers en dur selon des conditions internes, puis de mettre uniquement les valeurs externes comme paramètres préparés.

Sur des requêtes multi-filtres, cela demande un peu plus de code, mais c’est exactement le prix de la sécurité. Et quand le site grandit, ce surcoût se paie en tranquillité, parce que vous réduisez le temps perdu à traquer un bug de sécurité.

Que faire si vous héritez d’un code WordPress déjà en place ?

Si vous reprenez un projet existant, vous ne pouvez pas tout réécrire. Vous devez donc prioriser.

Je commence généralement par:

    les zones où des données externes sont traitées en base: recherche, filtres, endpoints AJAX, actions admin, les plugins ou thèmes qui contiennent des requêtes SQL custom, les zones “dynamiques” comme tri, pagination, filtres.

Puis je fais des corrections ciblées. Une fois que vous avez corrigé une injection, vous pouvez ensuite étendre la démarche à d’autres requêtes en suivant les mêmes patterns.

Un détail important: après correction, testez. Pas seulement le “cas normal”. Testez des valeurs limites, des valeurs vides, des caractères inattendus, et des tentatives de structures cassantes. Vous cherchez à confirmer que le code rejette ou traite proprement, sans transformer l’entrée en requête.

Les tests manuels peuvent suffire pour un premier niveau. Pour une approche plus robuste, l’idéal est d’ajouter des tests unitaires sur les fonctions de requêtes, mais sur WordPress, ce n’est pas toujours simple selon l’architecture du projet.

Et le WAF dans tout ça ?

Un WAF ou un module de pare-feu peut aider à réduire le nombre d’exploitations évidentes. Mais il ne doit pas devenir votre stratégie principale. Un WAF est un filet. La sécurité doit reposer sur la manière dont le code construit les requêtes.

Dans un scénario réel, un WAF peut:

    bloquer des patterns connus, ralentir des tentatives automatisées, réduire l’exposition pendant que vous corrigez.

Mais si la logique SQL est vulnérable, un attaquant peut parfois trouver des variantes de payload qui passent entre les mailles. La protection la plus durable reste l’approche WordPress côté code, requêtes préparées et validation stricte.

Récapitulatif opérationnel: la stratégie qui tient dans le temps

Prévenir l’injection SQL sur WordPress, ce n’est pas une case à cocher lors d’une mise en conformité. C’est une discipline technique qui combine les bons outils WordPress, des contrôles de validation, et une manière de coder qui n’autorise pas la concaténation de données externes dans le SQL.

Si je devais résumer l’approche qui marche le mieux sur des projets WordPress existants:

    vous traquez toutes les requêtes SQL custom, vous remplacez la concaténation par $wpdb->prepare, vous alignez le placeholder sur le type de la donnée, vous appliquez validation et bornes avant la préparation, vous durcissez l’accès aux endpoints et vous surveillez.

Le résultat, c’est un site moins fragile aux variations de code, aux plugins tiers, et aux changements futurs.

Si vous avez un extrait précis de code (un plugin ou une fonction qui construit une requête), vous pouvez le partager, et je peux vous indiquer où se situe le risque et comment le corriger de façon WordPress-friendly, sans casser la logique métier.