Une recherche qui affiche une erreur
Une fromagerie en ligne propose une barre de recherche dans son catalogue. Un visiteur tape Mont d'Or, un fromage très connu.
La page répond par une ligne de mots étranges, venus de la machine qui est derrière. Le gérant parle d'un petit bogue. C'est le signe que la page laisse le visiteur écrire une partie de ses questions.
La règle unique
Attention
Ce laboratoire est une boutique volontairement fragile. Attaquez votre copie privée, et rien d'autre : elle n'a aucune sortie vers Internet. Attaquer un système qui n'est pas le vôtre, ou sans permission de test, est un délit dans presque tous les pays.
À la fin de cette leçon
Remarque
Je peux corriger une requête pour que le texte tapé ne puisse plus la changer.
Mots-clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Base de données | un espace organisé où un programme range des fiches et les retrouve | |
| Requête | une question qu'un programme pose à une base de données | SELECT nom FROM produits |
| Injection SQL | un texte tapé qui change la requête envoyée par le programme | |
| Paramètre | une valeur envoyée à côté de la requête, jamais dedans | ? |
| Chaîne de caractères | un morceau de texte dans un programme, souvent appelé simplement chaîne | "comte" |
| Entrée | tout ce qu'un visiteur tape ou envoie à une page |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une requête fabriquée avec du texte
Beaucoup de programmes fabriquent une requête en collant un début fixe et le texte du visiteur. La base reçoit une seule longue chaîne. Elle ne sait pas quelle partie vient du développeur.
Si le texte contient une apostrophe, elle ferme trop tôt le texte du développeur. Ce qui suit est alors lu comme une partie de la requête.
La requête que le programme fabrique
# Remplacez terme par "Mont d'Or" puis lancez le code.
terme = "comte"
debut = "SELECT nom, prix FROM produits WHERE nom LIKE '%"
requete = debut + terme + "%'"
print(requete)
Exemple travaillé : trois recherches
| Le visiteur tape | La requête reçue par la base | Ce qui se passe |
|---|---|---|
| comte | ... LIKE '%comte%' | Tout marche comme prévu |
| Mont d'Or | ... LIKE '%Mont d'Or%' | L'apostrophe ferme le texte trop tôt : la requête est cassée |
| x' AND 1=2 -- | ... LIKE '%x' AND 1=2 --%' | Le visiteur a ajouté une condition à lui |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Regardez la boutique de votre laboratoire
Votre laboratoire contient une vraie boutique avec cette faiblesse. Démarrez-le, puis lancez ces trois commandes. Elles regardent seulement.
La première fait une recherche ordinaire. La deuxième envoie un nom avec une apostrophe, que la boutique ne sait pas traiter. La troisième affiche la ligne du code qui fabrique la requête.
Une recherche ordinaire
curl -s -G --data-urlencode "q=pen" http://app:8000/searchAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Un nom avec une apostrophe
curl -s -G --data-urlencode "q=Baker's" http://app:8000/searchAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
La ligne qui fabrique la requête
docker exec app grep -n "rows = db.execute" /tmp/app/app.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que disent les trois réponses
La première affiche Pen 1.2 : le nom et le prix. La deuxième affiche une ligne qui commence par database error. La boutique a collé votre apostrophe telle quelle dans sa requête.
La troisième affiche un numéro de ligne et une longue ligne de code. Repérez les signes + : chacun colle du texte tapé dans la requête.
Un paramètre garde le texte à part
Un paramètre règle le problème. Le programme envoie la requête avec un ? à la place de la valeur. Le texte tapé voyage séparément.
La base lit d'abord la requête et fixe sa forme. Le texte arrive ensuite et ne peut être qu'une valeur. Aucune apostrophe ne peut plus rien fermer.
La même recherche, avec un paramètre
import sqlite3
base = sqlite3.connect(":memory:")
base.execute("CREATE TABLE produits (nom TEXT, prix REAL)")
base.execute("INSERT INTO produits VALUES (?, ?)", ("Mont d'Or", 14.5))
base.execute("INSERT INTO produits VALUES (?, ?)", ("Comté", 9.9))
# Changez terme et lancez le code : rien de ce que vous tapez ne ferme la requête.
terme = "Mont d'Or"
lignes = base.execute("SELECT nom, prix FROM produits WHERE nom LIKE ?", ("%" + terme + "%",)).fetchall()
print(lignes)
Une erreur fréquente
Attention
Beaucoup pensent qu'il suffit de retirer les apostrophes du texte tapé. C'est faux : un filtre oublie toujours un cas, et un cas oublié est une porte ouverte. Un paramètre protège parce qu'il ne mélange jamais le texte à la requête. Utilisez-en un pour chaque valeur.
Certaines parties ne sont pas des valeurs
Un paramètre remplace une valeur. Il ne remplace ni un nom de colonne ni un sens de tri. Une page qui laisse trier par prix ou par nom doit pourtant écrire ce mot dans la requête.
La méthode sûre est une courte liste de mots permis. Si le choix du visiteur n'y figure pas, on prend une valeur par défaut.
Seulement les mots de la liste
PERMIS = ["nom", "prix"]
# Essayez choix = "prix", puis choix = "stock; vider", et lancez le code.
choix = "nom"
if choix in PERMIS:
colonne = choix
else:
colonne = "nom"
print("ORDER BY " + colonne)
Pour aller plus loin : le sens du tri
Une page de catalogue laisse souvent choisir aussi le sens du tri, croissant ou décroissant. Comme la colonne, ce mot ne peut pas être un paramètre.
Une fois le devoir réussi, copiez colonne_tri dans un bloc de code à vous. Écrivez sens_tri(choix), qui renvoie "ASC" ou "DESC" et "ASC" pour tout le reste. Essayez-la avec "desc", "DESC" et "DESC; vider", puis dites lesquels vous acceptez et pourquoi.