L’injection SQL, sur WordPress, n’a rien d’un scénario abstrait. Quand elle réussit, elle peut contourner l’authentification, lire des tables sensibles, modifier des données ou même préparer d’autres attaques. Ce qui rend la menace particulièrement frustrante, c’est que WordPress et ses extensions font déjà énormément de travail “standard” côté base de données. Le problème arrive souvent là où un plugin, un thème, ou même un bout de code maison construit une requête avec un paramètre utilisateur insuffisamment filtré.
Dans la pratique, empêcher l’injection SQL sur WordPress, ce n’est pas “rajouter un filtre et espérer”. C’est installer des garde-fous à plusieurs niveaux: utiliser les bonnes fonctions, valider et contraindre les entrées, verrouiller les droits en base, limiter l’exposition, et surveiller les requêtes qui ne devraient jamais exister. Et comme les sites varient beaucoup, il faut aussi accepter un point important: vous ne sécurisez pas seulement contre une seule ligne de code. Vous sécurisez contre l’ensemble des chemins d’exécution possibles, y compris ceux qui viennent de mauvais plugins ou de code modifié après coup.
D’où vient l’injection SQL dans un site WordPress
Une injection SQL fonctionne presque toujours sur le même mécanisme. Un développeur construit une requête SQL en concaténant une partie provenant de l’utilisateur. Tant que le paramètre reste “sage”, tout se passe bien. Dès qu’un attaquant injecte une chaîne conçue pour casser la structure SQL (guillemets, commentaires SQL, opérateurs), le moteur de base de données interprète le tout comme une instruction plus large que prévu.
Dans WordPress, les chemins classiques ressemblent à ceci:
Un formulaire (connexion, recherche, profil, contact, endpoints AJAX). Une action WordPress (admin-ajax.php, REST API, hooks). Une couche applicative qui appelle $wpdb ou exécute une requête via SQL brut. Un paramètre issu de $_GET, $_POST, de l’en-tête, ou d’un champ stocké dans la base, qui est réinjecté dans la requête sans encadrement solide.Je l’ai vu plus d’une fois sur des audits: le code n’était pas “monstrueux”. Souvent, une requête “simple” finissait par faire le faux pas. Par exemple, un plugin de gestion d’événements qui récupère un identifiant, puis fait un SELECT en collant le paramètre directement. L’exemple paraît inoffensif, jusqu’au moment où quelqu’un teste id=1' OR '1'='1.
L’autre aspect, plus discret: certains développeurs pensent se protéger avec sanitize_text_field() ou esc_sql(). Ces fonctions sont utiles, mais elles ne remplacent pas les requêtes préparées. Et surtout, elles ne s’appliquent pas toujours correctement si le code mélange types, encodages, ou concaténations “en dur”.
Les deux règles qui changent tout: requêtes préparées et typage
Si vous ne deviez retenir que deux principes, ce seraient ceux-là.
La première règle, c’est d’utiliser systématiquement les requêtes préparées de $wpdb. WordPress fournit des outils conçus pour ce cas d’usage, avec une protection contre l’interprétation SQL non voulue. C’est le mécanisme central pour empêcher l’injection SQL.
La seconde règle, c’est de contraindre le type de données. Un identifiant doit être un entier. Une limite doit rester dans un intervalle raisonnable. Un champ de tri ne doit accepter que des valeurs prédéfinies. Ce n’est pas seulement une question de sécurité, c’est aussi une question de robustesse: un système qui “accepte” des entrées incohérentes finit par entrer dans des états imprévus, y compris quand une injection n’est pas au premier plan.
Concrètement, ces erreurs reviennent souvent:
- Concaténer directement un paramètre dans une chaîne SQL: "WHERE id = $id". Préparer une partie de requête mais laisser un morceau en concaténation. Utiliser LIKE sans gérer correctement les caractères de l’utilisateur. Ne pas distinguer chaînes, entiers, booléens, et listes. Passer une variable “numérique” sous forme de texte, et s’appuyer sur une conversion implicite.
Ce que vous pouvez faire tout de suite dans WordPress (sans toucher au code)
Avant d’attaquer la partie code, il y a des actions concrètes qui réduisent fortement la surface d’attaque. Elles ne remplacent pas les correctifs applicatifs, mais elles donnent du temps et réduisent l’impact.
Le plus utile, c’est de traiter votre site comme un ensemble de composants, et pas comme un seul “WordPress core”. Les plugins, les thèmes et parfois même des mu-plugins jouent un rôle déterminant dans les requêtes SQL. Mettre à jour, désactiver ce qui ne sert pas, limiter l’accès à l’admin et sécuriser l’upload, tout cela diminue les chemins exposés à des paramètres contrôlés par un attaquant.
Ensuite, vous devez prêter attention aux requêtes suspectes. Sur un site WordPress standard, les traces SQL ne sont pas toujours visibles. Mais vous pouvez observer la situation via logs applicatifs, logs web, et, selon votre hébergement, des journaux de base de données. L’objectif n’est pas de “tout comprendre” en une fois, c’est d’identifier quels endpoints reçoivent des patterns inhabituels et quels plugins sont impliqués.
Enfin, pensez “conteneur” de l’attaque: si votre site autorise n’importe qui à envoyer des requêtes vers un endpoint, sans rate limiting, sans authent, ou avec trop de privilèges, l’injection devient plus facile à tester. Ce n’est pas la base de données seule qui fait le succès, c’est l’ensemble du pipeline.
Fixer le problème dans le code: patterns sûrs avec $wpdb
Quand vous corrigez un code, vous voulez une approche qui soit claire pour un relecteur humain. Les patterns WordPress existent justement pour éviter l’ambiguïté.
Imaginons une requête “naïve” écrite à la main pour sélectionner des entrées. Le risque est dans la concaténation d’une variable utilisateur. La correction, c’est de passer par $wpdb->prepare() et d’utiliser les formats attendus.
Le point pratique est de comprendre comment fonctionne le format. Dans $wpdb, vous pouvez indiquer le type attendu pour chaque variable via les placeholders, par exemple %d pour un entier et %s pour une chaîne. La logique interne gère la mise en forme et l’échappement.
Ce que je recommande sur des projets réels:
- Laisser WordPress gérer la sérialisation SQL via les placeholders, pas via votre propre concaténation. Rendre explicite le type, même si PHP peut parfois convertir automatiquement. Pour les clauses IN (...), ne pas fabriquer une parenthèse “à la main” avec des virgules collées. Utiliser des méthodes qui encadrent la liste de manière sûre (et, si la taille de liste peut exploser, imposer une limite).
Il y a un piège fréquent: la tentation de “préparer” une requête mais d’injecter un nom de colonne ou une direction ASC/DESC venant de l’utilisateur. Une requête préparée protège la valeur, pas les identifiants SQL libres. Un nom de colonne construit dynamiquement doit être validé contre une liste blanche de valeurs autorisées.
Je l’ai vu sur un filtre de tri de tableaux dans l’admin. Le code prenait orderby et order depuis $_GET, puis construisait ORDER BY $orderby $order. Même avec un prepare, on ne protège pas un identifiant si on le met tel quel dans la requête.
Dans ce cas, la correction passe par la validation: vous définissez un ensemble de colonnes autorisées, vous mappez les entrées à des valeurs sûres, et vous rejetez le reste.
Valider les entrées sans les transformer en faux sentiment de sécurité
Une bonne validation fait deux choses: elle empêche les valeurs absurdes d’entrer, et elle réduit la complexité que l’attaquant peut exploiter. Mais la validation seule n’est pas toujours suffisante si la suite du code fait une concaténation SQL.
Sur WordPress, la tentation est d’utiliser des fonctions de “sanitize” partout. Elles sont utiles, mais elles ne sont pas conçues comme un rempart universel contre le SQL. Une entrée peut être “nettoyée” d’un point de vue HTML, sans être correctement encadrée pour une requête SQL.
Voici une règle de terrain que j’applique quand je relis du code: si la variable finit dans une requête, je veux voir soit des placeholders (%s, %d), soit une méthode dédiée pour le cas précis. Si je ne vois pas de requêtes préparées, je pars du principe qu’il y a un risque. Même si la valeur a l’air propre.
La validation doit aussi gérer les contraintes métiers. Par exemple, un identifiant d’utilisateur ne devrait pas seulement être un entier. Il devrait aussi appartenir au bon contexte. Si un endpoint permet à un visiteur de consulter des informations, il ne suffit pas d’assurer que user_id est un entier, il faut vérifier les droits.
Encadrer les endpoints où l’entrée circule (REST, AJAX, formulaires)
L’injection SQL a souvent un chemin d’entrée évident: un paramètre d’URL ou un champ de formulaire. Mais les endpoints WordPress modernes élargissent les surfaces.
- admin-ajax.php est très utilisé pour les actions côté navigateur. Il suffit qu’un handler passe une valeur dans une requête pour créer une faille potentielle. L’API REST de WordPress reçoit des paramètres via JSON ou query. Les plugins qui exposent des endpoints custom sont parfois écrits comme un “mini-backend” où la validation est oubliée. Les formulaires frontaux passent rarement par des contrôles stricts, surtout quand on démarre vite un site.
Mon conseil pratique: listez les points d’entrée qui déclenchent des requêtes SQL côté serveur. Pour un plugin, ce sont souvent ses actions AJAX, ses endpoints REST et ses pages admin. Pour un thème, ce sont les requêtes liées aux recherches, aux filtres, et aux fonctionnalités “dashboard”.
Si vous avez une base de code existante, vous pouvez faire une recherche rapide dans le dépôt pour repérer les usages risqués: concaténations dans les requêtes, fonctions $wpdb->query() avec des chaînes construites, ou méthodes du type get_results("...$var..."). Ce n’est pas parfait, mais c’est efficace pour identifier où regarder.
Exemple de correction: identifiants et requêtes simples
Prenons un cas concret, courant, celui d’un plugin qui affiche des éléments par identifiant.

Le problème typique côté mauvais code: le développeur récupère $_GET['id'], puis construit une requête avec un ... WHERE id = $id .... Si id est une chaîne inattendue, ou pire, si l’attaquant injecte des caractères qui cassent la requête, l’impact peut aller au-delà de la simple erreur.
La correction robuste se fait en deux temps: d’abord convertir et valider le type (par exemple, forcer en entier), ensuite utiliser $wpdb->prepare() avec %d. Si, en plus, vous vérifiez que l’élément demandé correspond à ce que l’utilisateur a le droit de voir, vous ajoutez une couche de sécurité “verticale” qui ne dépend pas du SQL.
Et si le paramètre n’est pas un entier mais une chaîne, par exemple un slug, utilisez %s avec une validation “raisonnable” (taille maximale, format attendu). Une restriction de taille n’empêche pas l’injection à elle https://gardewp.fr/securite-wordpress/ seule, mais elle réduit les cas extrêmes qui rendent les erreurs plus probables.
Gérer les cas délicats: LIKE, clauses dynamiques et tri
Il y a des endroits où même les développeurs expérimentés hésitent.
Le cas LIKE
Quand vous faites WHERE nom LIKE '%$terme%', vous voulez deux choses: encadrer le paramètre et gérer les jokers. Un attaquant pourrait chercher à exploiter les jokers pour élargir une recherche, ou provoquer des comportements inattendus. Dans un modèle sûr, vous construisez le motif côté serveur, mais vous continuez à passer le paramètre via une requête préparée avec placeholder. Le motif lui-même inclut les caractères % côté code, tandis que le terme utilisateur reste traité comme une valeur.
Le tri et les colonnes
Comme je le disais plus haut, vous ne pouvez pas traiter un identifiant SQL comme une simple valeur. Si vous devez construire ORDER BY, vous mappez une valeur d’entrée vers une colonne autorisée. Même idée pour les expressions plus complexes: vous évitez les fragments libres construits à partir d’input.
Les tables et noms de colonnes dynamiques
Dans la plupart des plugins, c’est une mauvaise idée de laisser un paramètre sélectionner une table ou une colonne. Si vous avez vraiment besoin de configurer, faites-le via options administrateur, et validez strictement côté admin. Ne laissez pas le front, ni même un endpoint non authentifié, piloter ce genre de détails.
Durcir la base de données et les droits: la sécurité qui ne dépend pas du code
Même si vous corrigez le code, il reste un principe de défense en profondeur. La base de données doit répondre le moins possible.
Le point de doctrine est simple: un compte MySQL utilisé par WordPress ne devrait pas avoir des droits inutiles. En pratique, selon votre hébergement, WordPress utilise un utilisateur MySQL dédié, et il est important qu’il n’ait pas de privilèges trop larges comme DROP, ALTER ou SUPER. Votre hébergeur peut aussi avoir des restrictions et des configurations spécifiques, mais l’idée demeure.

Si un attaquant arrive à exécuter une injection SQL, des droits trop permissifs transforment une fuite en catastrophe. Sans droits dangereux, l’impact se limite davantage à des lectures et à des modifications plus proches des capacités attendues par WordPress.
Je n’affirme pas que tous les hébergeurs offrent la même granularité, mais dans les environnements sérieux, on peut au moins vérifier que les privilèges sont cohérents avec l’usage. Par exemple, un site ne doit pas avoir besoin d’un utilisateur capable de modifier le schéma.
Surveiller et réduire le temps de réaction
Empêcher l’injection SQL, c’est bien. Mais l’exploitation peut parfois passer à travers des angles morts. D’où l’importance de détecter rapidement les tentatives.
Sans faire de promesse irréaliste, vous pouvez surveiller plusieurs signaux:
- Requêtes HTTP avec paramètres anormalement longs, caractères spéciaux SQL fréquents, ou séquences typiques de payloads. Erreurs SQL dans les logs applicatifs ou de base. pics de trafic sur un endpoint AJAX ou un endpoint REST. échecs répétés de droits (si vos logs séparent bien les niveaux).
Sur WordPress, le mieux est de lier les logs à des événements applicatifs. Si un plugin exécute une requête et déclenche une erreur SQL, vous voulez savoir quel endpoint l’a provoquée. Ensuite, vous corrigez la zone de code.
Une mini check-list de corrections prioritaires
Si vous devez agir rapidement sur un site existant, voici une séquence raisonnable. Pas besoin de tout faire en une nuit, mais ces points sont ceux qui ont le meilleur rapport effort et réduction du risque.
- Mettez à jour WordPress, les plugins et le thème, et supprimez ou désactivez ceux qui ne sont pas indispensables. Recherchez les requêtes SQL construites par concaténation (dans vos plugins, thèmes custom et mu-plugins) et remplacez par des requêtes préparées avec $wpdb. Validez strictement les entrées qui alimentent un SQL, en imposant le type attendu, et refusez les valeurs hors format. Évitez tout identifiant SQL dynamique venant du client, comme ORDER BY ou le nom d’une colonne, au profit d’une liste blanche. Vérifiez les droits SQL du compte MySQL utilisé par WordPress pour limiter les dégâts en cas d’exécution malveillante.
Comment auditer un plugin ou un thème sans devenir fou
Dans un audit, l’objectif n’est pas de lire chaque ligne comme un roman. C’est de repérer les zones à risque. Voici comment je procède, parce que ça marche en conditions réelles.
Je commence par repérer les zones où l’entrée utilisateur peut arriver. Dans un plugin, c’est souvent dans:
- handlers AJAX, endpoints REST, pages admin qui stockent des options, shortcodes qui affichent des données, formulaires frontaux.
Ensuite, je remonte vers les appels à $wpdb. Le code dangereux est généralement identifiable à l’œil quand la requête est un string construit avec interpolation directe, ou quand on voit $_GET ou $_POST se frayer un chemin jusqu’au SQL sans étape intermédiaire de préparation.
Enfin, je fais une vérification “droits”. Même si le SQL est protégé, un endpoint peut permettre de lire des données appartenant à un autre utilisateur si les contrôles d’accès sont faibles. Ce n’est pas une injection SQL, mais c’est souvent le résultat final de la chaîne d’exploitation.
Il y a un bon compromis: concentrer l’effort sur les points d’entrée qui reçoivent des paramètres non authentifiés. Les endpoints authentifiés sont parfois plus simples à protéger, mais ils ne sont pas “sûrs” par défaut. Beaucoup de sites accordent des privilèges trop larges dans l’admin ou via des rôles custom.
Les tests qui valident vraiment la correction
Après correction, ne vous contentez pas de “ça marche”. Il faut valider que vous avez éliminé la classe de problème, pas juste corrigé un cas particulier.
Sur WordPress, vous pouvez valider en testant des valeurs qui cassent la structure SQL, puis en observant le comportement. Une bonne correction ne renvoie pas de résultats étranges, et ne provoque pas d’erreurs SQL.
Mais attention, je recommande de ne pas faire des tests destructifs sur un site de production. Utilisez un environnement de test, ou au minimum une instance de staging. Les tentatives d’injection peuvent déclencher des alertes, des blocages WAF, ou des erreurs temporaires. C’est normal, mais ça dépend de vos outils de sécurité.
Si votre hébergeur ou votre WAF bloque déjà certains payloads, l’absence d’erreur ne prouve pas l’absence de faille. Ce que vous voulez, c’est un code qui ne dépend pas d’un blocage extérieur, même si le WAF reste une couche utile.
Edge cases que je vois souvent lors des corrections
Quelques cas reviennent régulièrement, et ils expliquent pourquoi des sites “corrigés” peuvent rester vulnérables.
D’abord, les développeurs modifient une requête ailleurs, pas celle qui est réellement exécutée. Un plugin peut avoir plusieurs chemins d’exécution, et seule une branche a été corrigée.
Ensuite, les développeurs remplacent une concaténation par une préparation, mais laissent un identifiant ou un fragment SQL dynamique. Exemple typique: ORDER BY construit avec un paramètre non validé.
Puis, il y a les requêtes qui utilisent des morceaux de SQL “préparés” mais mélangés à des fragments string. Par exemple, une partie WHERE préparée et une autre partie concaténée. Ça suffit parfois à réintroduire le risque.
Enfin, certaines valeurs proviennent de la base, pas directement du client. Un attaquant peut d’abord injecter une donnée dans une table via un autre chemin, puis déclencher une requête plus tard en réutilisant cette donnée. Même si vous protégez le paramètre “aujourd’hui”, un stock “pollué” peut poser problème si le code utilise ces valeurs comme des fragments SQL.
Relier la sécurité WordPress au quotidien de l’équipe
La sécurité WordPress ne se limite pas à un correctif ponctuel. Ce qui tient dans la durée, c’est l’habitude.
Quand une équipe développe un plugin custom, il est utile d’avoir une règle simple: aucune requête SQL sans requête préparée quand il y a une variable issue de l’utilisateur. Et si un jour il faut construire un fragment SQL, on documente la liste blanche et les raisons.
Le même principe s’applique en revue de code. Une relecture rapide sur les points d’entrée et sur $wpdb évite souvent de laisser passer un bug.
Si vous administrez un site, votre “procédure” de maintenance compte. Les mises à jour ne sont pas un détail. Les plugins les plus anciens ou ceux qui ne sont plus maintenus sont des candidats fréquents pour des failles, parfois sur des chemins peu utilisés. Et plus le site est complexe, plus il est facile de ne pas remarquer qu’un shortcode ou un endpoint caché devient exposé après une modification.

Où mettre le curseur entre flexibilité et sécurité
Parfois, la correction demande des compromis.
Un plugin de recherche peut accepter des filtres variés, ce qui pousse à construire du SQL dynamique. La sécurisation demande soit une construction rigoureuse avec des placeholders partout, soit une refonte vers des options prédéfinies. La refonte coûte du temps. La construction rigoureuse coûte de la discipline.
Mon approche consiste à réduire la liberté offerte à l’entrée externe. Par exemple, au lieu de laisser un paramètre choisir n’importe quel champ, vous proposez un ensemble de filtres. Ça donne un SQL plus simple, et ça réduit les risques d’erreur. Pour un site où l’objectif est de livrer rapidement une fonctionnalité, ce choix est souvent acceptable parce qu’il limite aussi la complexité future.
La sécurité n’empêche pas de faire des choses. Elle impose juste de faire les choses avec des frontières plus nettes.
Résultat attendu: moins de surface d’attaque, plus de résilience
Quand l’injection SQL est correctement traitée, vous observez des effets concrets.
Les requêtes SQL ne doivent plus dépendre d’une concaténation de l’utilisateur. Les erreurs SQL devraient disparaître ou devenir exceptionnelles. Les endpoints doivent refuser les valeurs inattendues au lieu d’essayer de “deviner” ce que vous vouliez dire. Et surtout, l’impact d’une tentative malveillante doit se limiter à un échec, pas à une escalade.
Sur un site WordPress, c’est exactement ce qu’on veut: pas seulement “empêcher”, mais empêcher sans casser le reste, tout en gardant une base solide pour les évolutions.
Si vous avez un plugin ou un extrait de code concerné, je peux aussi vous aider à le revoir de manière ciblée, en indiquant les endroits où la requête doit passer en $wpdb->prepare() et où une liste blanche est nécessaire (comme pour le tri ou les colonnes).