Qu'est-ce qu'une requête paramétrée ?
Une requête paramétrée est une requête SQL où les valeurs sont envoyées à part, au lieu d'être collées dans le texte. La base de données sait alors toujours ce qui est la requête et ce qui est une valeur.
C'est la méthode qui arrête l'injection SQL. Vous allez apprendre à l'écrire, à la reconnaître dans le code d'un autre, et à construire une requête à critères variables sans rien risquer.
À la fin de cette leçon
Remarque
Je peux écrire une requête où aucune donnée venue de l'extérieur n'entre dans le texte de la requête.
Mots-clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Requête paramétrée | une requête dont les valeurs voyagent séparément du texte | WHERE titre = ? |
| Marqueur | le signe qui tient la place d'une valeur dans la requête | ? |
| Injection SQL | un texte venu de l'extérieur qui change le sens de la requête | |
| Échappement | le fait de neutraliser à la main les signes spéciaux d'un texte | |
| Liste permise | la liste des mots acceptés pour une partie qui ne peut pas être un marqueur | |
| Compte de la base | l'identité sous laquelle le programme se connecte à la base |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
La règle unique
Attention
Ce laboratoire est une boutique volontairement fragile. Travaillez sur votre copie privée, et rien d'autre : elle n'a aucune sortie vers Internet. Tester un système qui n'est pas le vôtre, ou sans permission, est un délit dans presque tous les pays.
Coller du texte dans une requête est dangereux
Un programme qui écrit sa requête avec un signe + mélange deux sortes de texte : le sien et celui du visiteur. La base reçoit une seule chaîne et ne sait plus les séparer.
Le texte du visiteur peut alors se faire passer pour une partie de la requête. Ce n'est pas une question de talent de l'attaquant, c'est un défaut de construction.
Deux façons d'écrire la même recherche
import sqlite3
base = sqlite3.connect(":memory:")
base.execute("CREATE TABLE livres (titre TEXT, auteur TEXT)")
base.execute("INSERT INTO livres VALUES (?, ?)", ("L'Étranger", "Camus"))
base.execute("INSERT INTO livres VALUES (?, ?)", ("La Peste", "Camus"))
mot = "Peste"
# Mauvaise : le mot est collé dans le texte avec +.
collee = "SELECT titre FROM livres WHERE titre LIKE '%" + mot + "%'"
# Bonne : le mot voyage à part, à la place du ?.
propre = "SELECT titre FROM livres WHERE titre LIKE ?"
print(base.execute(collee).fetchall())
print(base.execute(propre, ["%" + mot + "%"]).fetchall())
Échapper à la main ne suffit pas
On pourrait doubler chaque apostrophe du texte tapé avant de le coller. C'est de l'échappement. Il marche parfois et échoue souvent : un seul signe oublié, un seul codage de caractères inattendu, et la porte s'ouvre.
Un marqueur n'a pas ce problème. Il ne transforme pas le texte, il l'empêche d'être lu comme de la requête.
Reconnaître une requête paramétrée dans du code
Lire du code est la moitié du métier. Une requête sûre a des ? à la place des valeurs, et une liste de valeurs à côté. Une requête fragile a un + ou un % qui colle une variable.
Le programme de votre laboratoire contient les deux sortes. Lancez ces deux commandes, qui regardent seulement : la première cherche la fonction de recherche, la seconde les endroits qui écrivent plusieurs lignes d'un coup.
La ligne où commence la recherche
docker exec app grep -n "def search" /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.
Les endroits qui utilisent déjà des marqueurs
docker exec app grep -n "executemany" /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.
Le programme sait déjà écrire des marqueurs
La première commande affiche 27:def search(term):. La seconde affiche deux lignes, la 15 et la 20, où le programme écrit ses propres produits et ses comptes avec des ?.
Ces lignes sont sûres. Cherchez maintenant la ligne de la fonction de recherche : elle n'a pas de ?. Voilà la différence à repérer en revue de code.
Un marqueur remplace une valeur, pas un mot
Un marqueur peut tenir la place d'un nombre ou d'un texte. Il ne peut pas tenir la place d'un nom de colonne, d'une table ou d'un sens de tri.
Pour ces parties, on écrit le mot soi-même, mais seulement après l'avoir comparé à une liste permise. Le visiteur choisit parmi vos mots, il n'écrit jamais les siens.
Exemple travaillé : une requête construite par morceaux
| Morceau | D'où vient-il ? | Comment l'ajouter ? |
|---|---|---|
| SELECT titre FROM livres WHERE disponible = 1 | Écrit par le développeur | Texte fixe |
| AND auteur = ? | Écrit par le développeur, quand un auteur est demandé | Texte fixe, valeur dans la liste |
| L'auteur lui-même | Saisi par le visiteur | Dans la liste des valeurs, jamais dans le texte |
| ORDER BY titre | Choisi parmi deux mots permis | Mot de la liste permise, jamais le texte du visiteur |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Limiter les droits du compte de la base
Même bien écrit, un programme peut contenir une faille qu'on n'a pas vue. La seconde ligne de défense est le compte sous lequel il parle à la base.
Un site de recherche n'a besoin que de lire quelques tables. S'il ne peut ni écrire ni lire les comptes des clients, une injection réussie rapporte beaucoup moins. C'est le moindre privilège, appliqué à la base.
Une erreur fréquente
Attention
Beaucoup pensent qu'un format ressemblant à une requête paramétrée en est une. Par exemple, une phrase écrite avec la mise en forme de Python, f"... {mot}", colle le texte tout autant qu'un plus. Un marqueur n'est vrai que si la valeur part dans la liste à côté.