flawopen.com/Injection SQL/Python

Injection SQL en Python

Critique CWE-89 Brouillon — en attente de relecture
Language: English Deutsch Español Français हिन्दी Bahasa Indonesia 日本語 한국어 Português (Brasil) Русский 简体中文
Explication simple

Imaginez un formulaire qui n'attend qu'un numéro de billet, comme 482. L'injection SQL se produit quand quelqu'un tape autre chose qu'un nombre dans cette case — une formule piégée qui fait dire au système « montre-moi tous les billets » au lieu de simplement le billet 482, parce que le système n'a jamais vérifié que ce qu'il recevait était bien un nombre.

Termes clés de cette page
entrée contrôlée par l'utilisateur
Toute valeur provenant, en dernier ressort, de la personne qui utilise — ou attaque — l'application : un champ de formulaire, un paramètre d'URL, le nom d'un fichier envoyé, un en-tête HTTP. L'application ne peut pas supposer qu'elle est bien formée ou sûre.
requête SQL
La commande envoyée à une base de données — par exemple « récupérer cette ligne », « supprimer cette table ». Son sens dépend entièrement de son texte exact, ce qui est précisément ce qui rend dangereuse l'injection de texte supplémentaire.

Ce qui se passe

L'injection SQL se produit quand une entrée contrôlée par l'utilisateur est insérée directement dans le texte d'une requête à la base de données, au lieu d'être transmise comme une valeur séparée. Si le code construit une requête en collant des chaînes de caractères, un attaquant peut fournir une entrée qui modifie la structure réelle de la requête — transformant une requête « récupérer une ligne » en une requête qui renvoie toutes les lignes, ou qui supprime une table.

En Python, cela se présente presque toujours de la même façon : un appel à la base de données construit avec une f-string, un formatage %, ou une concaténation avec +, au lieu des marqueurs de paramètres intégrés au driver de la base.

Impact concret

En 2015, l'opérateur britannique TalkTalk a subi une fuite de données touchant plus de 150 000 clients après que des attaquants ont exploité une faille d'injection SQL dans une page web héritée d'une acquisition. Le régulateur britannique de la protection des données a infligé une amende de 400 000 £ à TalkTalk, qualifiant la faille d'évitable et de basique.

Source : avis de sanction de l'Information Commissioner's Office du Royaume-Uni, 2016 — voir les références ci-dessous.

Vulnérable vs. corrigé

VULNÉRABLE
# user_id vient directement de la requête
def get_user(cursor, user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    cursor.execute(query)
    return cursor.fetchone()
CORRIGÉ
# la valeur est passée séparément, jamais insérée
def get_user(cursor, user_id):
    query = "SELECT * FROM users WHERE id = %s"
    cursor.execute(query, (user_id,))
    return cursor.fetchone()

Pourquoi la correction fonctionne

La version corrigée transmet le texte de la requête et la valeur à execute() comme deux arguments distincts. Le driver de la base de données les envoie lui aussi séparément à la base — la structure de la requête est fixée avant que la valeur n'y soit attachée, si bien que la valeur ne peut jamais être interprétée comme faisant partie de la syntaxe SQL, quels que soient les caractères qu'elle contient. Une f-string ne peut pas offrir cela : au moment où execute() reçoit la requête, la valeur est déjà intégrée au texte comme si elle avait toujours fait partie de la commande.

Particularités propres à Python

Le formatage % dans execute() ressemble encore à du paramétrage — mais n'en est pas

cursor.execute("... WHERE id = %s" % user_id) est tout aussi vulnérable qu'une f-string. Le marqueur ne devient sûr que lorsque la valeur est passée comme le second argument d'execute() lui-mêmecursor.execute("...WHERE id = %s", (user_id,)) — afin que ce soit le driver, et non le formatage de chaînes de Python, qui effectue la substitution.

Les ORM paramètrent par défaut, mais pas leurs échappatoires

L'ORM de Django et le query builder de SQLAlchemy paramètrent automatiquement pour les requêtes normales. Le risque revient dès que vous utilisez Model.objects.raw() ou text() de SQLAlchemy en construisant ce SQL brut avec une f-string.

La syntaxe des marqueurs n'est pas uniforme entre les drivers

psycopg2 (PostgreSQL) utilise %s quel que soit le type de colonne ; sqlite3 utilise ?. Copier le style de marqueur de la documentation d'un driver vers un autre échoue silencieusement — vérifiez le style de paramètre propre à votre driver plutôt que de le supposer.

Idées reçues courantes

« Mon ORM me protège automatiquement »

Vrai pour l'API de requête normale de l'ORM — faux dès que vous utilisez raw() ou text() et construisez cette chaîne vous-même.

« Cet identifiant est toujours un nombre, donc l'interpoler est sans risque »

Le risque ne tient pas au type de la valeur à l'exécution — il tient au fait que la requête est construite par interpolation de chaîne. Dès que cette hypothèse cesse d'être vraie à un moment de la vie du code, la vulnérabilité est déjà là, en attente.

« J'échappe moi-même les guillemets, donc je n'ai pas besoin de requêtes paramétrées »

L'échappement manuel dépend du driver et se trompe facilement de façon subtile. Les requêtes paramétrées ne sont pas une version plus stricte de l'échappement — elles évitent le problème entièrement, car la valeur ne fait jamais partie du texte de la requête.

Comment vérifier si vous êtes concerné

grep -rn "execute(f\"" --include="*.py" . grep -rn "execute(.*%\s*(" --include="*.py" . grep -rn "\.raw(\|text(" --include="*.py" .
Mieux que grep seul : exécutez Bandit (règle B608, hardcoded_sql_expressions) en CI — il détecte ce motif automatiquement et fait échouer le build en cas de nouvelle occurrence.

Checklist de prévention

Questions fréquentes

Utiliser un ORM me protège-t-il de l'injection SQL ?

Oui, pour ses méthodes de requête normales. Ses échappatoires pour requêtes brutes non — elles sont exactement aussi sûres que du SQL écrit à la main, pas plus.

Ce risque ne concerne-t-il que des éléments comme les champs de recherche ?

Non. Tout ce qui est effectivement contrôlé par un tiers compte — en-têtes HTTP, noms de fichiers envoyés, ou même une valeur provenant d'une API tierce en laquelle votre application a confiance.

Puis-je simplement échapper les guillemets moi-même ?

Vous le pouvez, mais c'est fragile et dépendant du driver. Les requêtes paramétrées sont la véritable correction, pas une version plus stricte de l'échappement.

Références

Voir en : Python JavaScriptGoJava PHPC#Ruby C/C++RustKotlin Swift Solidity (N/A)
Voir aussi : Injection de CommandePath Traversal XSSDésérialisation Non Sécurisée