Sécurité WordPress : audit des paramètres de requête (query vars)

Quand on parle de sécurité WordPress, on pense souvent à l’authentification, aux plugins, aux permissions. Pourtant, une grande partie des bugs qui deviennent exploitables commencent plus en amont: comment WordPress interprète une requête HTTP, quels paramètres il transforme en variables de requête, et comment le code en aval les consomme.

Ces paramètres, dans le vocabulaire WordPress, prennent souvent la forme de “query vars”. Selon les cas, ce sont des clés issues de l’URL (GET et rewrite rules), des valeurs décodées par le parseur de requête, ou des variables ajoutées par WordPress et par les extensions via des APIs comme register_query_var(). Un audit de ces query vars n’est pas un exercice théorique. Il sert à repérer des surfaces d’entrée inattendues, des valeurs non prévues, et des comportements divergents entre front et back, entre HTTP normal et endpoints spécifiques (REST, admin-ajax, etc.).

Je vous propose une méthode réaliste d’audit et de sécurisation, basée sur ce que l’on observe dans des environnements WordPress courants: multisites, permaliens complexes, plugins qui ajoutent des rewrite rules, et thème qui surcharge des requêtes via pre_get_posts ou des templates qui déduisent des permissions à partir de la requête.

Clarifier le concept: “query vars” et pourquoi ça compte

Une requête HTTP arrive dans WordPress, puis WordPress exécute une chaîne de traitement. Grosso modo:

WordPress normalise la requête et la découpe (chemin, paramètres, règles de rewrite). Il construit un ensemble interne de “query variables” qui serviront ensuite à décider quoi afficher et comment construire la requête (par exemple, quel type de contenu, quel auteur, quel taxonomie). Les hooks et le code applicatif lisent ces variables pour produire la réponse.

Le point de sécurité, c’est que ces variables deviennent très vite des “faits” dans le reste du cycle: un plugin ou un thème peut faire une logique de branchement sur une query var, parfois en supposant que la valeur a déjà été validée. Or, ce n’est pas toujours le cas.

On distingue plusieurs catégories utiles à auditer.

    Les query vars core, nativement supportés par WordPress (par exemple des variables liées aux pages, à certains types de contenu, etc.). Les query vars ajoutées par WordPress via ses endpoints (REST, admin-ajax, et d’autres patterns selon la config). Les query vars créées ou exposées par des plugins et le thème (via rewrite rules, paramètres de requête custom, add_rewrite_rule, register_query_var, et parfois des manipulations directes sur $wp->query_vars).

En pratique, ce qui pose problème n’est pas l’existence des query vars, mais leur combinaison avec du code qui “fait confiance” à leur format, leur rôle ou leur portée.

Les endroits où les query vars se matérialisent

Le piège classique est de croire que “si je sécurise $_GET et $_POST, c’est bon”. Sur WordPress, $_GET n’est qu’une source parmi d’autres. Une URL propre avec des permaliens peut ne pas contenir de GET explicite, et pourtant finir avec des valeurs dans $wp->query_vars après parsing.

Voici les points où l’on peut observer ce qui se passe.

1) Parse de la requête et rewrite rules

Les permaliens transforment une partie de l’URL en “arguments”. Par exemple, une règle de rewrite peut capturer un segment et le transformer en variable. Si une règle est trop permissive, ou si elle ne contraint pas correctement les expressions, vous pouvez vous retrouver avec des valeurs “non attendues” qui deviennent des query vars.

En audit, je regarde surtout:

    les règles ajoutées par les plugins, la façon dont elles capturent les segments, et si les variables produites correspondent à des types attendus (slug, entier, date, identifiant, etc.).

2) Variables issues de la query string

Là, la surface d’entrée est plus directe. Certaines variables seront simplement lues depuis $_GET si WordPress les mappe, ou si un plugin s’en sert sans passer par une validation stricte.

Attention au fait que WordPress peut parfois normaliser, filtrer partiellement, ou au contraire laisser passer des chaînes qui finissent dans un traitement plus tardif (construction de SQL, appels API, routes internes).

3) Variables “déclarées” via des APIs

Des extensions déclarent parfois des query vars via register_query_var(). C’est une bonne pratique fonctionnelle, mais en sécurité ça crée une promesse implicite: “cette variable existe et peut être lue”. Si le code qui l’utilise n’applique pas une politique de validation claire, la variable devient une entrée sensible.

4) Consommation par des hooks

Les hooks sont un point clé, parce que le comportement dépend du contexte. Par exemple:

    pre_get_posts et posts_where peuvent injecter des clauses conditionnées par des query vars. template_redirect peut déclencher des redirections selon une query var. des plugins peuvent remplacer des comportements en fonction de la présence ou de la valeur d’un paramètre.

Si une query var est influencée par l’URL, alors la logique des hooks doit être considérée comme potentiellement contrôlable.

Audit: transformer l’intuition en méthode

L’idée n’est pas de “tout lister” à la main. Sur un WordPress réel, ce serait interminable, et surtout fragile. L’approche qui marche le mieux consiste à combiner une cartographie rapide avec une vérification ciblée.

Étape 1: construire une liste de query vars réellement utilisées

L’objectif ici est de repérer:

    les query vars qui viennent de l’extérieur (URL, query string, rewrite), celles qui sont réellement lues par le thème et les plugins, et celles qui déclenchent des comportements visibles (affichage, redirection, exécution d’un handler).

Dans mes audits, je procède souvent en trois couches: observation, instrumentation temporaire, puis revue du code autour des points d’entrée.

L’instrumentation temporaire est utile, mais elle doit rester prudente. L’erreur fréquente est d’afficher des valeurs en production ou de journaliser des données trop sensibles. Une journalisation structurée, en environnement de staging si possible, limite le risque.

Étape 2: tester les limites de format

Une query var n’est pas seulement “présente” ou “absente”. Le plus intéressant est de tester:

    valeurs vides, chaînes longues, encodage URL (percent-encoding), caractères inattendus (barres, espaces, null bytes selon comment PHP les traite, ou simplement ponctuation), et types coercés (par exemple un entier attendu, mais une chaîne alphabétique fournie).

Ces tests révèlent les écarts entre validation implicite et validation explicite.

Par exemple, un code qui fait un cast (int) $value peut “fonctionner” mais ouvrir un contournement logique: abc devient 0, et 0 déclenche une branche non testée.

Étape 3: analyser la “trajectoire” des valeurs

Une query var est une variable, mais la sécurité se joue sur la trajectoire. Je pose ces questions:

    Cette valeur est-elle utilisée pour construire une requête SQL ? Si oui, via quels types de préparation (prepared statements, escape, whitelist) ? Est-elle utilisée pour choisir une classe, un fichier, une route, ou un template ? Est-elle utilisée pour calculer des droits, ou pour accéder à des objets WordPress (post, user, term) ? Cette valeur passe-t-elle par sanitize_text_field, absint, sanitize_title, esc_url_raw, ou autre chose ? Ou reste-t-elle brute trop longtemps ?

Cette trajectoire est souvent la différence entre un paramètre “inoffensif” et un vrai vecteur d’attaque.

Étape 4: vérifier les différences entre contextes

WordPress n’exécute pas tout le même code partout. Un paramètre peut être “bénin” sur le front mais dangereux dans un endpoint. Je vérifie notamment:

    pages publiques vs /wp-admin/ (même si l’admin est protégé, certains hooks peuvent se déclencher), REST API vs pages classiques, admin-ajax.php si des plugins exposent des actions qui dépendent d’une query var, et multisite, où la logique de domaine peut modifier le mapping des requêtes.

Les erreurs fréquentes (celles qui reviennent sur le terrain)

Voici ce que j’observe le plus souvent quand on fait un audit et sécurisation WordPress centré sur les query vars.

Confiance implicite dans la valeur

Un thème ou plugin lit une query var et assume qu’elle est valide. Exemple typique: si une query var représente un identifiant de post, le code va chercher le post puis décider quoi afficher. Si l’identifiant peut devenir 0, un post “par défaut” peut être exposé, ou une branche peut sauter une vérification de permission.

Absence de validation côté “entrée” (request parsing)

Même si le reste du code fait un esc_html à la sortie, il manque parfois une validation au point d’entrée. L’encodage n’empêche pas une logique incorrecte, ni une condition qui déclenche une requête plus large que prévu.

Escaper côté sortie est indispensable, mais ce n’est pas une validation de logique.

Whitelist manquante sur des paramètres qui pilotent des comportements

Quand une query var choisit un mode de rendu ou un type de contenu, sans whitelist, tout devient possible: ?mode=... ou ?tab=... et des branches qui ne devraient pas exister. Même si ça ne produit pas une exécution de code directe, ça peut produire des informations non prévues.

Rewrite rules trop permissives

Une règle de rewrite peut capturer trop large, et finir par alimenter une variable qui n’est pas prévue pour ce contexte. Le plugin “croît” que la valeur est un slug, alors qu’elle contient autre chose, et la validation arrive trop tard.

Contrôler ce qui est “officiel” vs ce qui est “dérivé”

Un réflexe utile consiste à distinguer les query vars reconnues et celles qui émergent indirectement.

Si un plugin s’attend à recevoir une query var “officielle” (déclarée, documentée, utilisée dans ses propres règles), la validation a souvent été pensée. En revanche, lorsqu’une variable est dérivée d’un segment d’URL ou d’un paramètre générique, elle est plus susceptible de passer par des chemins moins testés.

Dans les audits que je mène, je traite les query vars en trois niveaux de risque:

    niveau élevé: variables utilisées pour choisir des objets (post, user, term) ou pour construire des clauses conditionnelles, niveau moyen: variables utilisées pour filtrer, paginer, ou afficher des contenus déjà autorisés mais avec une logique de filtre, niveau faible: variables uniquement utilisées pour du rendu cosmétique, tant qu’elles ne touchent pas à des chemins internes.

Le point important: ce classement n’est jamais théorique. Il dépend du code qui lit ces variables.

Sécuriser concrètement: quoi faire, sans casser WordPress

Sécuriser les query vars ne veut pas dire rendre WordPress “rigide” au point de casser les permaliens, ni rendre les règles de rewrite inutilisables. L’objectif est de réduire l’espace des valeurs, et de garantir que toute utilisation suit une politique cohérente.

1) Valider au plus proche de la source

Si un paramètre pilote un comportement, validez tôt. En pratique, on peut:

    convertir un identifiant avec absint et vérifier qu’il est strictement supérieur à 0, appliquer sanitize_title si une query var représente un slug, utiliser filter_var sur les types qui s’y prêtent (booléens, entiers, formats), ou mieux, basculer vers une whitelist stricte pour les modes.

Le compromis: whitelist stricte peut casser des valeurs “exotiques” qui étaient acceptées. Mais c’est précisément le but de la sécurisation quand ces valeurs n’ont aucune raison métier d’exister.

2) Ne jamais confondre escape et validation

esc_html et esc_url empêchent l’injection dans le rendu HTML, mais ne protègent pas contre une logique incorrecte plus en amont. Si une query var influence la sélection de contenu, la validation doit précéder.

image

3) Utiliser les API WordPress pour les objets, pas des requêtes bricolées

Si vous devez récupérer un post par identifiant, passez par les APIs WordPress qui gèrent les aspects attendus. Ensuite, appliquez les checks de capacité quand c’est requis. Là encore, le piège est de récupérer un objet puis d’oublier qu’on ne doit pas l’afficher selon le contexte.

4) Mettre en place des garde-fous côté logique de permission

Une query var ne doit jamais servir de justification à elle seule pour accéder à un contenu. Elle doit être considérée comme un index d’intention, puis suivie par une vérification de permission.

Sur du code existant, je recommande une approche progressive: ajouter les contrôles là où l’exposition est la plus probable, puis élargir.

image

5) Réviser les rewrite rules et les contraintes

Un bon rewrite rule ne capture pas plus que ce qu’il doit capturer. Si une règle est censée capturer un entier, forcez la contrainte pour éviter que des chaînes arbitraires deviennent des query vars. Une contrainte d’expression régulière plus stricte, même si elle paraît “cosmétique”, réduit la quantité de valeurs anormales qui atteignent le reste du code.

Un exemple de logique à corriger (sans dramatiser)

Imaginons un plugin ou une surcharge de thème qui fait quelque chose comme: “si ?author=... est présent, afficher l’auteur correspondant”.

Le risque se matérialise si le code:

https://gardewp.fr/securite-wordpress/
    convertit mal la valeur, ou ne vérifie pas correctement les droits, ou autorise un chemin alternatif quand la conversion échoue.

La version robuste ressemble à une suite d’étapes, sans magie:

    récupérer la valeur, valider le format (entier strict, plage raisonnable), chercher l’objet, et vérifier la permission ou la logique d’accès en fonction du rôle et du contexte.

C’est le genre de correction qui ne casse pas WordPress, mais qui ferme énormément de possibilités “bizarres” utilisées par des attaquants.

Contrôle des query vars sensibles: quoi surveiller en priorité

Vous voulez une liste courte, utile, et pas un catalogue infini. J’en donne une, limitée à cinq idées, basées sur des comportements qu’on rencontre souvent.

    Variables qui sélectionnent un identifiant d’objet (post, user, term) et déclenchent une récupération directe. Variables qui pilotent un mode de rendu (templates, actions, tabs, “preview”). Variables liées à des filtrages puissants (date, type, taxonomie) quand le code élargit trop les résultats. Variables qui affectent la pagination ou des offsets, en particulier si le code utilise ces valeurs pour construire une requête. Variables utilisées pour décider d’une redirection ou d’un endpoint interne.

Si vous n’avez le temps que pour une passe rapide, démarrez par là.

Instrumentation légère: voir ce que WordPress voit vraiment

Il y a une différence entre “ce qu’on pense envoyer” et “ce que WordPress enregistre”. Pour auditer correctement les query vars, je privilégie une instrumentation qui capture un échantillon sans exposer de secrets.

Sur un staging, vous pouvez ajouter un enregistrement temporaire qui log:

    l’URL complète ou au moins le chemin et la query string, la liste de $wp->query_vars après parsing, et le moment où les hooks de thème ou plugins se déclenchent.

Le piège est de logguer en production, surtout si vous incluez des valeurs personnelles ou des cookies. Un mode temporaire, désactivé vite, et une rotation des logs sont non négociables.

Dans les cas où vous ne pouvez pas modifier le code, une approche externe consiste à comparer le comportement de pages avec des paramètres “quasi équivalents”. Par exemple, une valeur fournie en GET peut produire un résultat différent de la même valeur fournie via un permalien. Ce genre de différence signale souvent un mapping incomplet ou une branche spéciale.

Réduire la surface: limiter les query vars exposées

Une sécurité pragmatique, ce n’est pas seulement valider. C’est aussi réduire ce qui peut être influencé.

Quelques leviers réalistes:

    Désactiver ou retirer les endpoints dont vous n’avez pas besoin (notamment si des plugins exposent des actions ajax ou des routes internes dépendantes de query vars). Réviser les plugins qui ajoutent des rewrite rules. Tous ne sont pas conçus avec la même rigueur. Vérifier les paramètres publics documentés par vos plugins internes. Si un paramètre “fonctionnait” par accident, il n’a pas besoin de continuer à fonctionner.

Le compromis, encore une fois, est la compatibilité: une restriction brutale peut casser des usages légitimes. La bonne pratique est de restreindre graduellement, en surveillant les erreurs 404 ou les variations de comportement, sur une période courte.

Edge cases qui coûtent cher si on les ignore

Encodage et caractères spéciaux

Les slugs et segments d’URL passent parfois par des transformations (urldecode, normalisation). Une query var peut recevoir une valeur qui “semble” propre mais contient des variations encodées. Sans validation stricte, certaines branches de code peuvent interpréter différemment.

Valeurs vides et annulations

Un paramètre présent mais vide, ou un paramètre présent plusieurs fois, produit des comportements parfois surprenants selon comment le code récupère la valeur. PHP ne donne pas toujours le même résultat selon $_GET['x'] avec plusieurs occurrences. Là, la validation doit explicitement gérer le cas.

Coercition implicite

Une conversion en entier peut transformer “100abc” en 100, ou “abc” en 0. Ces cas sont des mines de logique. Si une query var représente un identifiant, exigez un format strict, ou au minimum vérifiez une plage.

Différence entre “paramètre inconnu” et “paramètre attendu”

Parfois, WordPress ignore une query var inconnue. Mais certains plugins peuvent traiter des valeurs même si la clé n’est pas déclarée. D’où l’intérêt d’auditer les plugins qui manipulent $_GET ou $wp->query_vars directement.

Une petite feuille de route de sécurisation (sans se perdre)

Voici comment j’organise un plan d’action quand on veut réellement avancer, sans noyer l’équipe.

Mettre en staging un environnement le plus proche possible de la prod, y compris les plugins et permaliens. Cibler les query vars lues par le thème et les plugins, via instrumentation ou revue de code autour des hooks. Tester les valeurs limites et les variations d’encodage pour repérer les branches logiques non sécurisées. Corriger en validant au point d’entrée et en appliquant des checks de permission quand une query var sélectionne des objets. Retester, puis mettre une surveillance légère sur les erreurs et les 404 pour repérer les effets de bord.

Si vous devez choisir, le meilleur “retour sur effort” se trouve presque toujours dans la validation des identifiants et les permissions, parce que ce sont ces points qui peuvent transformer une requête “bizarre” en exposition.

Cas particulier: sécuriser aussi le REST et les endpoints liés à la requête

Même si le sujet porte sur WordPress “request parameters”, je recommande de regarder la cohérence globale. Un plugin peut exposer un traitement REST qui, lui, dépend d’une query var côté URL, puis applique ensuite une logique de permissions insuffisante.

Le signe typique est le suivant: une page front montre des informations, mais un endpoint associé renvoie plus. Dans un audit, je relie les deux et je traite la logique d’accès comme un tout.

Ce que je vérifierais avant de dire “c’est bon”

Un audit sérieux ne se termine pas avec une modification “à la louche”. Je valide surtout quatre critères:

    Les query vars influencent-elles encore des comportements sensibles via des chemins non validés ? Les valeurs attendues sont-elles contraintes en format et en plage ? L’échappement de sortie est-il complété par une validation de logique ? Les permissions sont-elles vérifiées indépendamment de la présence d’une query var ?

À ce stade, vous pouvez aussi documenter ce que vous avez décidé. La documentation est une sécurité indirecte, car elle évite qu’un plugin futur réintroduise une dépendance à une query var “qu’on pensait safe”.

Checklist rapide avant déploiement

    Vérifier que chaque query var utilisée pour sélectionner un objet est validée (format, plage) avant récupération. Confirmer que la permission est checkée selon le contexte, pas seulement selon la requête. Reserrer les rewrite rules et les expressions qui capturent des segments, quand c’est faisable. Tester les cas vides, encodés et “presque valides” (coercition et valeurs inattendues). Surveiller logs et 404 après déploiement pour détecter les régressions sur les permaliens.

Finalement, pourquoi cet audit est payant

Auditer les query vars peut sembler une tâche de bas niveau, un détail dans le grand puzzle WordPress. Sur le terrain, c’est souvent l’un des rares endroits où vous pouvez réduire la surface d’entrée sans dépendre uniquement d’un plugin “qui corrige les vulnérabilités”.

image

Vous gagnez deux choses. D’abord, vous identifiez des surfaces d’entrée concrètes, celles que l’URL peut réellement pousser dans WordPress. Ensuite, vous imposez une discipline de validation et de permission qui rend les bugs moins exploitables, même quand le code n’est pas parfait.

C’est exactement l’esprit de l’audit et sécurisation WordPress: renforcer la logique aux bons endroits, plutôt que empiler des protections sans compréhension.

Si vous me dites votre contexte (single ou multisite, plugins principaux, et un ou deux exemples d’URL qui semblent “piloter” des comportements), je peux vous proposer une grille d’audit plus ciblée, avec les endroits du code où regarder en premier.