flawopen.com/Injection SQL/Python
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.
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.
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.# 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()
# 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()
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.
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ême — cursor.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.
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.
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.
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.
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.
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.
grep -rn "execute(f\"" --include="*.py" .
grep -rn "execute(.*%\s*(" --include="*.py" .
grep -rn "\.raw(\|text(" --include="*.py" .
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.
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.
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.