Durcissement WordPress : protéger contre l’injection SQL

Une injection SQL dans WordPress n’arrive presque jamais par magie. Elle apparaît quand un bout de données utilisateur finit mal traité dans une requête SQL, ou quand un plugin développe mal ses méthodes d’accès aux données. En pratique, le durcissement WordPress contre l’injection SQL, ce n’est pas seulement “installer un pare-feu”. C’est un empilement de choix techniques qui rendent la classe d’attaque difficile à exploiter, puis la rendent visible si quelque chose dérape.

Dans les incidents que j’ai vus, le point commun n’était pas forcément la présence d’un grand bug au cœur de WordPress. C’était plutôt une combinaison: un plugin “annexe” qui manipule des paramètres URL, un formulaire de recherche qui laisse passer un morceau de texte, et un développeur qui a utilisé une requête construite à la main. Ensuite, l’attaquant tente d’abord des entrées simples, puis des variantes plus agressives. Quand le modèle mental “ce paramètre ne peut pas contenir de SQL” est faux, l’attaque commence.

Comprendre le mécanisme, sans folklore

L’injection SQL repose sur un principe simple: si une application construit une instruction SQL en concaténant des morceaux (par exemple un paramètre GET ou POST) sans utiliser de requêtes préparées, alors une entrée malveillante peut modifier la logique de la requête.

Prenons un scénario courant sur un site WordPress:

    un endpoint lit un paramètre orderby pour trier une liste, un autre lit search pour filtrer, un troisième récupère un identifiant d’élément passé dans une requête.

Si ces valeurs sont injectées dans du SQL brut, l’attaquant peut tenter de fermer une chaîne, puis ajouter une condition. Parfois, il cherche à extraire des informations. Parfois, il cherche juste à contourner une validation. Il peut aussi provoquer des erreurs pour en déduire la structure.

Le danger, c’est que l’injection SQL n’exige pas forcément des privilèges “admin”. Elle peut suffire à lire des lignes d’autres tables, à contourner des vérifications, ou à déclencher des comportements inattendus dans le code applicatif.

Là où WordPress se défend déjà (et là où ça ne suffit pas)

WordPress fournit déjà des https://gardewp.fr/securite-wordpress/ garde-fous. Quand vous utilisez correctement l’API WordPress (par exemple WP_Query pour les requêtes de contenu, ou les fonctions de base pour les options et les métadonnées), vous réduisez drastiquement le risque de requêtes SQL construites à la main.

Mais il y a une réalité: votre site WordPress n’est pas juste WordPress. C’est WordPress plus vos thèmes, vos plugins, vos bibliothèques, parfois des “mu-plugins” activés par des automatismes, plus un peu de custom code. Dès qu’un plugin sort du cadre et appelle wpdb avec une requête assemblée, ou utilise une fonction de bas niveau de manière approximative, la surface augmente.

Le durcissement WordPress contre l’injection SQL, c’est donc deux objectifs:

Réduire la probabilité qu’un paramètre non maîtrisé arrive dans du SQL construit. Limiter l’impact si un plugin fait malgré tout une erreur.

La première partie relève du code et de la configuration. La seconde relève du cloisonnement, de la supervision, et d’une hygiène d’exploitation.

La ligne de démarcation: requêtes préparées, pas de concaténation

Quand on parle d’injection SQL dans l’écosystème WordPress, le rempart central se résume en une phrase pratique: utiliser les requêtes préparées et les mécanismes d’abstraction WordPress au lieu de concaténer.

Si vous développez ou auditez un plugin, cherchez les patterns suivants dans le code:

image

    "$sql = "SELECT ... " . $var . " ..."; construit à la volée, wpdb->query($sql) ou wpdb->get_results($sql) sans passage par une préparation, prepare mal utilisée, par exemple sans correspondance stricte entre formats et variables, des appels où le développeur “nettoie” l’entrée avec une fonction de type sanitize_text_field, puis la réinjecte quand même telle quelle dans du SQL brut.

Un point qui surprend souvent: même si vous “nettoyez” une chaîne, cela ne remplace pas la paramétrisation. La suppression de certains caractères peut réduire des vecteurs, mais elle ne prouve pas que la requête est sûre face à toutes les formes d’entrées.

Validation et sanitation: distinguer les responsabilités

Dans les projets sérieux, j’ai tendance à séparer mentalement deux couches:

    validation: est-ce que la donnée a un format attendu, un type attendu, et une plage attendue? sanitation: neutralise ou nettoie une donnée pour la rendre acceptable dans un contexte donné.

La validation concerne souvent le “contrat” de votre paramètre. Par exemple, un identifiant doit être numérique, un tri doit être dans une liste fermée, une date doit respecter un format. La sanitation sert ensuite à rendre la donnée inoffensive dans un contexte d’affichage, de stockage, ou de transmission.

En SQL, la sanitation seule n’est pas une stratégie. La paramétrisation reste la base. En revanche, une validation stricte réduit la surface et évite d’envoyer au code des valeurs inattendues qui déclenchent des branches imprévues.

Un exemple concret: si un paramètre id est censé être un entier positif, valider dès le début avec un cast propre et une vérification d’appartenance à une plage peut éviter des requêtes erronées et des chemins latéraux. Ce n’est pas “anti-injection” à lui seul, mais c’est un durcissement solide.

Le rôle de wpdb: ce que j’attends quand j’audite

$wpdb est puissant, mais il peut être dangereux s’il est utilisé comme simple concaténateur. Dans une revue de code, je regarde trois choses.

D’abord, est-ce que les paramètres sont passés via prepare avec des placeholders? Ensuite, est-ce que les types correspondent au placeholder (entier, chaîne, etc.)? Enfin, y a-t-il des cas où la logique SQL doit varier, par exemple un champ à trier ou une direction de tri?

C’est sur ce dernier point que des erreurs apparaissent fréquemment. Beaucoup de gens paramètrent la valeur à rechercher, mais laissent ORDER BY ou le nom d’une colonne être concaténé. Or, les paramètres ne peuvent pas remplacer librement un identifiant SQL de la même façon qu’une chaîne. On doit plutôt utiliser une whitelist: une liste de colonnes autorisées, et choisir uniquement parmi celles-ci.

Si vous devez construire un fragment SQL variable, vous pouvez le faire proprement en sélectionnant dans une liste fermée, puis en assemblant uniquement avec des éléments contrôlés. C’est un compromis raisonnable, parce que vous ne “paramétrez” pas un identifiant arbitraire, vous validez en amont.

Un cas fréquent dans WordPress: AJAX, REST et endpoints “sur-mesure”

Les injections SQL aiment les surfaces qui acceptent des entrées arbitraires: endpoints AJAX (admin-ajax.php) et REST API, formulaires qui stockent ou filtrent, pages de recherche, exports, endpoints de synchronisation.

Beaucoup de sites ont des requêtes qui ne passent pas par une UI. L’utilisateur n’interagit pas avec un champ visible, il envoie directement des paramètres. Cela change la dynamique: les équipes ne voient pas toujours les cas extrêmes, et les règles de validation peuvent être incomplètes.

Dès que vous avez un handler AJAX ou REST qui lit des paramètres pour faire une requête SQL, le niveau d’exigence doit monter. En pratique, je recommande de:

    valider chaque paramètre avec un type attendu, refuser les valeurs hors liste pour les champs non libres (tri, colonnes, opérateurs), utiliser prepare pour les valeurs, vérifier les permissions, pas seulement l’entrée.

Les permissions ne bloquent pas l’injection SQL en soi, mais elles limitent l’impact. Un handler accessible à tout le monde est un amplificateur de risque.

Durcissement WordPress: au-delà du code, la configuration compte

Une injection SQL exploitée reste une exploitation. Vous pouvez donc aussi réduire l’impact, même si un bug existe quelque part.

Sur WordPress, les leviers “non code” les plus efficaces sont:

    limiter les privilèges de l’utilisateur MySQL utilisé par WordPress, segmenter l’accès réseau (si vous avez la main sur l’infrastructure), réduire la divulgation d’erreurs, maintenir les plugins et thèmes à jour, surveiller les requêtes anormales côté base de données (quand c’est possible).

Je ne dis pas que “désactiver les erreurs” stoppe une injection SQL. Mais quand une requête échoue et que les erreurs SQL détaillées ne remontent pas dans une page, vous réduisez la capacité de l’attaquant à itérer. C’est souvent ce qui sépare une tentative bruyante d’un exploit plus patient.

Le piège des “templates” et des requêtes construites dans des hooks

WordPress encourage l’extension via des hooks et des filtres. C’est une excellente idée, mais ça peut créer des pièges si le code dans un hook assemble une requête à partir d’une valeur de paramètre global.

Le problème devient particulièrement visible avec:

    des filtres qui modifient directement des requêtes, des plugins qui interceptent des variables “query string”, des fonctions custom qui traitent des paramètres sans passer par une couche de validation centralisée.

Une bonne hygiène consiste à centraliser la validation des entrées dans une fonction de votre code, puis à ne jamais réutiliser une valeur brute dans un contexte SQL. Le gain est énorme, parce que vous supprimez les divergences de traitement.

Mettre en place une hygiène d’audit: quoi tester, sans se mentir

On peut tester “au hasard” des payloads SQL, mais c’est rarement utile. Le durcissement doit être orienté vers vos points d’entrée réels.

Quand j’audite un site WordPress pour l’injection SQL, je commence par cartographier les paramètres qui finissent dans du SQL. Ensuite, je vérifie s’ils passent par une préparation ou une construction sûre. Enfin, je fais des tests de régression sur les endpoints identifiés.

Voici un mini plan d’audit pragmatique, limité et orienté action:

    Identifier les endpoints qui acceptent des entrées (GET, POST, AJAX, REST) et tracer où ces valeurs sont utilisées. Rechercher dans le code l’usage de $wpdb avec des requêtes concaténées. Vérifier les cas où le SQL varie (tri, colonnes, opérateurs) et s’assurer qu’ils passent par une whitelist. Contrôler la gestion des permissions sur les endpoints non publics. Couper la fuite d’erreurs sensibles en production pour réduire le feedback à un attaquant.

Ce que j’aime dans ce plan, c’est qu’il ne dépend pas d’une liste universelle de payloads. Il dépend de votre réalité applicative.

Protection MySQL: limiter l’impact si quelque chose passe

Même si vous mettez tout en place côté applicatif, un attaquant peut trouver un angle mort. D’où l’intérêt de réduire le pouvoir de l’utilisateur base de données.

En général, WordPress n’a pas besoin de droits “bizarres” comme FILE ou des privilèges trop larges. Selon votre configuration, l’utilisateur MySQL utilisé par WordPress peut être limité à un ensemble minimal (lecture et écriture sur les tables nécessaires). Je conseille de ne pas laisser un compte “admin MySQL” par commodité.

Une autre couche utile, si vous avez une visibilité raisonnable, consiste à instrumenter. Si vos équipes savent lire les logs MySQL (ou au moins les métriques), elles peuvent détecter des schémas de tentatives (beaucoup de requêtes échouées, patterns récurrents). Ce n’est pas une solution magique, mais c’est un accélérateur de réponse.

Les compromis qui reviennent toujours

Sur le terrain, on finit toujours par équilibrer sécurité, performance et maintenance.

Par exemple, passer partout sur prepare ne coûte pas “rien”, mais c’est rarement le facteur principal. En revanche, ajouter des whitelists pour des champs dynamiques demande un peu de discipline côté code. La bonne nouvelle, c’est que ces whitelists rendent aussi le code plus clair et plus testable.

Autre compromis: certains plugins ajoutent des fonctionnalités de recherche très flexible. Si vous refusez trop de paramètres, vous cassez l’usage. Si vous acceptez trop de variabilité, vous augmentez le risque. La solution pratique consiste à offrir de la flexibilité “fonctionnelle” (par exemple plusieurs filtres), mais de limiter ce qui devient “SQL dynamique” (colonnes et opérateurs). Le SQL doit rester sous contrôle.

Exemple typique d’erreur et correction attendue

Imaginons un plugin qui ajoute un champ “tri” depuis un paramètre sort. Il fait ensuite:

    soit une concaténation directe: ORDER BY $sort soit une requête préparée uniquement pour la valeur de filtre, mais pas pour ORDER BY.

Le premier cas est un chemin direct vers l’injection, parce que le SQL inclut un fragment non contrôlé. Le second cas peut déjà être amélioré, mais il reste un risque si sort influence un fragment SQL.

La correction attendue n’est pas forcément “aucune concaténation”. Elle est “concaténation contrôlée”: vous définissez une liste de valeurs autorisées, vous mappez chaque valeur sur une expression SQL acceptable, puis vous ne concaténez que la version contrôlée.

Même idée pour un filtre par champ: au lieu d’accepter un champ arbitraire, vous acceptez seulement des alias connus. Le client choisit un alias, pas un nom de colonne.

Durcissement WordPress: mettre à jour sans déclencher l’enfer

La mise à jour des plugins et thèmes est souvent présentée comme une évidence. En sécurité, c’est vrai, mais ça se gère.

Les mises à jour peuvent contenir des corrections de sécurité, y compris des bugs liés à des injections ou à des manipulations d’entrées. Le risque, c’est l’indisponibilité.

Une méthode que j’ai vue fonctionner dans des contextes où on a plusieurs sites, c’est un processus simple:

    tester les mises à jour sur un environnement proche (mêmes versions PHP, même configuration de base, mêmes données ou un échantillon), déployer en fenêtre de maintenance, observer les logs applicatifs et les erreurs 4xx/5xx après déploiement.

Ce n’est pas glamour, mais c’est ce qui empêche que “la sécurisation” devienne la source d’un incident.

Monitoring et réponse: quand une tentative ressemble à une autre requête

Si vous surveillez seulement la disponibilité, vous ratez des signaux de sécurité. L’injection SQL laisse parfois des traces indirectes: des requêtes échouées, des patterns répétés, des erreurs inattendues dans le code applicatif.

Selon votre stack, vous pouvez surveiller:

    les journaux d’accès web (plusieurs requêtes sur le même endpoint, avec des longueurs inhabituelles), les logs applicatifs PHP (exceptions, erreurs de types, messages de préparation), les logs base de données (trop d’erreurs, requêtes répétitives).

Le but n’est pas de paniquer à chaque alerte. Le but est d’avoir une réponse structurée. Une tentative d’injection détectée doit déclencher une enquête sur l’endpoint ciblé, sur les versions des plugins impliqués, et sur la présence de routes qui construisent du SQL à partir d’entrées non contrôlées.

Checklist de durcissement: une version mentale, pas un document figé

Je préfère une checklist plus “vérifications récurrentes” que “one shot”. Sur un site qui évolue, les plugins changent, et les endpoints aussi.

Gardez en tête ces principes, en particulier pour l’injection SQL, en les transformant en habitudes de maintenance:

    chaque entrée utilisateur, même “interne”, mérite une validation de type et une contrainte de valeurs, tout usage de SQL direct doit passer par des mécanismes sûrs et vérifiables (requêtes préparées), les fragments SQL dynamiques doivent être choisis dans des listes fermées, les erreurs sensibles ne doivent pas être exposées aux visiteurs, les privilèges base de données doivent être minimisés, et l’actualisation des dépendances doit rester disciplinée.

Si vous appliquez ces règles, vous ne rendez pas WordPress invulnérable, mais vous réduisez la probabilité d’une exploitation réussie, et vous diminuez énormément l’impact quand un bug apparaît.

Et si le risque vient d’un plugin tiers?

C’est un cas très fréquent. Vous ne contrôlez pas le code du plugin, mais vous subissez sa surface.

Deux approches sont possibles. La première est d’éviter le plugin: chercher une alternative maintenue, plus transparente, ou un réglage qui limite ses fonctionnalités. La seconde est l’encadrement: isoler l’usage du plugin, limiter les endpoints exposés, réduire les permissions, et renforcer la détection autour.

Quand vous suspectez un plugin, je recommande de commencer par identifier précisément les fonctionnalités en cause. Par exemple, un formulaire de recherche interne ou un export déclenche-t-il des erreurs SQL? Un endpoint admin est-il accessible de manière inattendue? Une requête côté base montre-t-elle des échecs répétitifs quand on teste un paramètre?

Une fois que vous avez ce lien, vous pouvez prioriser. Et souvent, ce travail de priorisation vaut autant que des semaines de tests “au hasard”.

Ce que j’attends d’un site “bien durci” sur ce sujet

Un site WordPress bien durci contre l’injection SQL ressemble moins à un château fort et plus à une série de barrières logiques. Il y a du code propre, oui. Mais il y a aussi de la discipline d’exploitation, et surtout une capacité à repérer ce qui dévie.

Quand l’équipe comprend où se trouvent les entrées, comment elles sont validées, et comment le SQL est construit, vous gagnez quelque chose de précieux: la sécurité devient diagnostiquable. Et quand une injection apparaît, elle ne reste pas une énigme. Elle devient un morceau de code à corriger, un endpoint à verrouiller, ou un plugin à remplacer.

Le durcissement WordPress, dans cette perspective, n’est pas une mode. C’est une façon de rendre vos mises à jour moins risquées, de vos évolutions plus sûres, et de votre base applicative plus prévisible face à des entrées hostiles.