?tri=prix ASC se transforme en ?tri=prix; DROP TABLE wp_annonces-- : c’est cette manipulation, testée lors d’un audit sur un moteur de recherche immobilière développé en extension WordPress sur mesure, qui a révélé une injection SQL directement exploitable via un paramètre a priori anodin.
Le moteur de recherche permettait de trier les résultats selon plusieurs critères — prix, surface, date de publication — chacun associé à un paramètre d’URL transmis tel quel à la construction de la requête SQL, sans passer par aucune validation préalable de sa valeur.
Le point aveugle d’une requête pourtant préparée
Le reste de la requête utilisait correctement $wpdb->prepare() pour les critères de filtrage, comme le prix minimum ou la ville recherchée. Mais la clause ORDER BY était construite par simple concaténation, avec la conviction erronée qu’une requête « préparée » protégeait l’ensemble de l’instruction SQL, quelle que soit la façon dont chacune de ses parties était assemblée.
function rechercher_annonces( $ville, $tri ) {
global $wpdb;
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}annonces WHERE ville = %s ORDER BY " . $tri,
$ville
);
return $wpdb->get_results( $sql );
}
Cette confusion technique est fréquente : $wpdb->prepare() paramètre des valeurs, jamais des identifiants comme un nom de colonne ou un mot-clé de direction de tri. Un paramètre concaténé directement dans la chaîne SQL, même entouré d’un appel à prepare() pour d’autres parties de la requête, reste exposé exactement comme s’il n’y avait aucune protection.
Ce que l’attaque a réellement permis
Lors du test contrôlé en environnement de recette, la valeur transmise dans le paramètre tri a permis d’exécuter une sous-requête arbitraire, révélant la structure complète de la table des utilisateurs par une injection de type UNION SELECT. Le moteur de base de données ne fait aucune distinction entre une clause de tri légitime et une instruction malveillante insérée au même endroit : il exécute ce qu’on lui soumet.
- Le paramètre de tri provenait directement de l’URL, sans aucune liste de valeurs acceptées.
- Aucune fonction d’échappement adaptée aux identifiants SQL n’était appliquée.
- La présence de
$wpdb->prepare()ailleurs dans la requête donnait une fausse impression de sécurité globale.

La correction : une liste blanche, pas un échappement
Un nom de colonne ou une direction de tri ne se paramètrent pas comme une valeur ordinaire. La seule approche fiable consiste à valider la valeur reçue contre une liste explicite de possibilités autorisées, avant toute construction de la requête, en rejetant silencieusement toute valeur qui n’y figure pas.
function rechercher_annonces( $ville, $tri_demande ) {
global $wpdb;
$colonnes_autorisees = array(
'prix_asc' => 'prix ASC',
'prix_desc' => 'prix DESC',
'surface' => 'surface DESC',
'recent' => 'date_publication DESC',
);
$tri = $colonnes_autorisees[ $tri_demande ] ?? 'date_publication DESC';
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}annonces WHERE ville = %s ORDER BY $tri",
$ville
);
return $wpdb->get_results( $sql );
}
La valeur insérée dans la requête finale ne provient plus jamais directement de l’utilisateur : elle provient d’un tableau défini dans le code, indexé par une clé fournie par l’utilisateur. Toute valeur absente de ce tableau retombe sur un tri par défaut, sans jamais atteindre la construction SQL.
Un défaut qui dépasse ce seul moteur de recherche
Ce type de paramètre — tri, direction, parfois nom de colonne d’affichage — apparaît fréquemment dans les interfaces de recherche ou de tableau de données, précisément parce qu’il semble éloigné des données sensibles habituellement associées à l’injection SQL, comme un identifiant ou un mot de passe. Cet éloignement apparent explique pourquoi il échappe souvent aux revues de sécurité centrées sur les champs de saisie évidents.
| Paramètre | Traitable par prepare | Nécessite une liste blanche |
|---|---|---|
| Valeur de filtre (ville, prix) | Oui | Non |
| Nom de colonne de tri | Non | Oui |
| Direction de tri (ASC / DESC) | Non | Oui |
Repère retenu de cet audit : dès qu’un paramètre influence la structure d’une requête plutôt que sa valeur, il ne relève plus du même mécanisme de protection, et mérite une vérification explicite avant toute construction de la clause concernée.
Notre verdict
La présence de $wpdb->prepare() dans une requête ne garantit rien sur les parties de cette même requête construites par ailleurs. Chaque fragment assemblé mérite d’être examiné individuellement : un identifiant de colonne ou une direction de tri ne se protègent jamais de la même façon qu’une valeur littérale, et une liste blanche explicite reste la seule réponse fiable à ce type de paramètre.