vendredi 25 septembre 2026

À propos

Contact

Sécurité

$wpdb->prepare et injections SQL : les pièges que même les pros font encore

LIKE, IN(), types de placeholders mal choisis : $wpdb->prepare protège des injections SQL seulement s'il est utilisé correctement. Voici les erreurs classiques.

Par Clément Hadrot • 14 janvier 2021 • 5 min de lecture • Aucun commentaire
$wpdb->prepare et injections SQL : les pièges que même les pros font encore

Les injections SQL restent, des années après leur découverte, l’une des vulnérabilités les plus graves qu’une extension ou un thème WordPress puisse introduire. Une requête mal construite peut permettre à un attaquant de lire l’intégralité de la base de données, y compris la table des utilisateurs et leurs mots de passe hashés, ou de la modifier directement. WordPress fournit un outil simple pour s’en prémunir, $wpdb->prepare(), mais son usage comporte des pièges que je retrouve régulièrement en audit de code, y compris chez des développeurs expérimentés.

Le principe de base est connu : ne jamais insérer une variable directement dans une chaîne SQL. Ce qui est moins connu, ce sont les cas particuliers où prepare() est mal utilisé tout en donnant l’illusion d’être sécurisé.

Le problème de base : la concaténation directe

Voici l’exemple classique de code vulnérable, du genre qu’on retrouve encore dans certaines extensions anciennes :

// Dangereux : ne jamais faire ça
$id = $_GET['id'];
$resultats = $wpdb->get_results(
    "SELECT * FROM {$wpdb->prefix}mes_donnees WHERE id = $id"
);

Si $id contient autre chose qu’un nombre, par exemple 1 OR 1=1, la requête change complètement de sens et peut renvoyer l’intégralité de la table. La correction consiste à passer par $wpdb->prepare(), qui échappe et type correctement chaque valeur :

$id = absint( $_GET['id'] );
$resultats = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}mes_donnees WHERE id = %d",
        $id
    )
);

Notez que le préfixe de table, {$wpdb->prefix}, n’a pas besoin d’être préparé puisqu’il ne provient jamais d’une entrée utilisateur ; seules les valeurs variables issues d’une requête, d’un formulaire ou d’un cookie doivent transiter par prepare().

Bien choisir son placeholder

$wpdb->prepare() accepte trois types de placeholders, et se tromper de type est une source fréquente de bugs, parfois de failles :

  • %d pour un entier, converti automatiquement en nombre entier même si la valeur d’origine était une chaîne ;
  • %f pour un nombre à virgule flottante ;
  • %s pour une chaîne de caractères, la plus utilisée et la plus sûre par défaut car elle échappe correctement les caractères spéciaux SQL.

Utiliser %s pour un identifiant numérique fonctionne, mais utiliser %d pour un texte le tronque silencieusement à zéro ou à la première suite de chiffres trouvée, ce qui casse la requête sans erreur visible. En cas de doute sur le type réel d’une donnée, %s reste le choix le plus sûr.

L'essentiel à retenir : Une requête SQL avec une variable concaténée directement est une faille potentielle ; Les placeholders %s, %d et %f ne sont pas interchangeables ; LIKE et IN() demandent une construction particulière du prepare

Le piège du LIKE

Construire une clause LIKE en insérant directement des caractères % dans la valeur avant de la passer à prepare() casse la préparation, car prepare() utilise lui-même le caractère % comme marqueur de placeholder. La bonne méthode utilise $wpdb->esc_like() avant de construire le motif :

$recherche = $wpdb->esc_like( $_GET['s'] );
$motif = '%' . $recherche . '%';

$resultats = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
        $motif
    )
);

esc_like() échappe les caractères % et _ présents dans la valeur de recherche elle-même, avant que le % de concaténation ne soit ajouté pour construire le motif de recherche. Sans cette étape, un utilisateur qui saisit un % ou un _ dans le champ de recherche obtient un comportement inattendu, voire une requête cassée.

Le piège du IN()

Une clause IN() avec un nombre variable de valeurs ne peut pas recevoir un seul placeholder pour toute la liste. Il faut générer autant de placeholders que de valeurs, puis les passer un par un :

$ids = array( 4, 15, 23 );
$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );

$resultats = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->posts} WHERE ID IN ($placeholders)",
        $ids
    )
);

La fonction array_fill() génère ici un placeholder %d par élément du tableau, puis prepare() accepte un tableau comme second argument, qu’elle éclate automatiquement pour remplir chaque placeholder dans l’ordre.

Erreurs fréquentes en revue de code

  • Appeler prepare() mais ne jamais utiliser son résultat dans la requête réellement exécutée ;
  • Mélanger des valeurs déjà échappées manuellement avec esc_sql() et un appel à prepare(), ce qui double l’échappement et corrompt les données ;
  • Passer un tableau ou un objet directement en paramètre sans le sérialiser, provoquant une erreur silencieuse ou un comportement inattendu ;
  • Oublier que les noms de colonnes et de tables ne peuvent pas être des placeholders : ils doivent être validés autrement, par exemple via une liste blanche de valeurs autorisées.

En revue de code, dès que je vois une requête SQL construite avec une double quote contenant une variable PHP directement entre accolades, je m’arrête et je vérifie systématiquement si un prepare() a été utilisé plus loin. C’est souvent là que se cachent les failles les plus sérieuses d’une extension.

En résumé

$wpdb->prepare() reste l’outil de référence pour se prémunir des injections SQL dans WordPress, mais il ne protège que s’il est utilisé pour chaque valeur variable, avec le bon type de placeholder, et avec les précautions particulières qu’exigent LIKE et IN(). Une requête SQL construite à la main sans passer par prepare() doit systématiquement déclencher un signal d’alarme en revue de code, quelle que soit la confiance qu’on accorde à l’origine de la donnée.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi