Requêtes sûres
- Leçon 8
- 50 min
- Module 3 : Accéder aux données depuis une API
Leçon 8 sur 16 · Bases de données et stockage pour les applications d'IARequêtes sûres
1. Choisir le stockage selon la charge de travail
2. Concevoir un schéma relationnel
3. Accéder aux données depuis une API
4. Améliorer la performance des requêtes
5. Fichiers, vecteurs et cycle de vie
Ce que vous saurez faire
Assurez-vous que le texte d’une requête ne peut jamais modifier le SQL exécuté par l’API. Regroupez ensuite tout le SQL de l’API dans un fichier, et voyez ce qu’un ORM changerait.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les paramètres nommés et la chaîne f qui a brisé une requête, dans Une extraction reproductible du cours SQL et préparation des données pour l’IA. Cette leçon va plus loin : un serveur, une deuxième table et une API devant.
- Les routes, modèles et modules d’un projet FastAPI, dans Séparer les responsabilités du cours Créer et déployer votre première API.
- Les clés étrangères de la migration
005, dans Contraintes d’intégrité, ainsi que la validation et l’annulation des transactions, dans Transactions.
Ce qu’il vous faut sur votre ordinateur
- Le projet
~/projects/ticket-apià la fin du module 2.python -m pytestaffiche53 passed(53 tests réussis). - PostgreSQL en marche (
docker compose up -d db), avec les petites données chargées (python -m scripts.load --reset). - Le dépôt des exercices dans
~/projects/nextia-learning-labs, comme l’indique son README des instantanés. Vous copiez des fichiers de l’instantanéend-of-m03.
Les temps de ce module proviennent d’un Apple M4 avec 16 Go, sous macOS 26.6, avec Python 3.12. PostgreSQL 18.6 et pgvector 0.8.7 tournent dans Docker. Vos temps seront différents. Les commandes Windows et Linux sont écrites, mais n’ont pas été exécutées. Chaque entrée de cette leçon s’exécute sur votre propre base de données locale.
Un champ dans un formulaire
Section intitulée « Un champ dans un formulaire »Vous remplissez un formulaire à la banque. Un champ indique « Votre nom ». Vous écrivez Olga Olsen. L’employé lit le champ et cherche ce nom. Peu importe ce que vous écrivez, l’employé le traite comme un nom.
Imaginez maintenant un employé négligent. Il ne lit pas le champ comme un nom. Il copie vos mots dans une phrase et la lit au coffre : « Apportez le dossier du client nommé Olga Olsen. » Si vous écrivez nobody, and also every other file (personne, ainsi que tous les autres dossiers), la phrase devient : « Apportez le dossier du client nommé personne, ainsi que tous les autres dossiers ». Le coffre obéit à la phrase.
L’employé prudent sépare deux choses : l’instruction, écrite par la banque, et la valeur, écrite par vous. Cette séparation est le sujet de cette leçon.
Le problème : du texte qui devient du SQL
Section intitulée « Le problème : du texte qui devient du SQL »Amira écrit la première recherche de la nouvelle API : trouver un client par son adresse courriel. Sa première version construit le texte SQL avec une chaîne f, comme dans bien des tutoriels :
def find_customer_unsafe(conn: psycopg.Connection, email: str) -> list[tuple]: # DO NOT DO THIS: the text becomes part of the SQL. sql = f"SELECT customer_id, name, email FROM customers WHERE email = '{email}'" print(f" SQL sent: {sql}") return conn.execute(sql).fetchall()Avec une adresse normale, cela fonctionne. Le script scripts.injection_demo exécute cette version et une version sûre côte à côte, sur votre propre base de données :
unsafe (string building): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = 'olga.olsen@example.com' 1 row(s), first: ('C-0001', 'Olga Olsen', 'olga.olsen@example.com')Sam demande ce qui arrive avec une adresse étrange. L’entrée x' OR '1'='1 est le test classique. Son premier guillemet ferme la chaîne que le code d’Amira a ouverte. Le reste devient du SQL :
unsafe (string building): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = 'x' OR '1'='1' 40 row(s), first: ('C-0001', 'Olga Olsen', 'olga.olsen@example.com')La recherche d’un seul client a retourné les 40 clients. La clause WHERE a maintenant deux conditions, et '1'='1' est vrai pour chaque ligne. Une entrée plus dangereuse lit une autre table. UNION ajoute les lignes d’une deuxième requête SQL à la première, et -- transforme le reste de la ligne en commentaire :
unsafe (string building): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = 'x' UNION SELECT ticket_id, subject, body FROM tickets --' 200 row(s), first: ('T-30098', 'Recycling old item', 'Can you take back my old step ladder when you deliver the new one?')Une recherche de client a retourné tous les billets de soutien, avec leur texte complet. Dans un vrai service de soutien, ce sont les messages privés de tous les clients. Cette attaque s’appelle une injection SQL : du texte externe s’insère dans le SQL et modifie son action.
L’auteur a aussi essayé une deuxième instruction, dans un test conçu sur la base de données locale :
unsafe (string building): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = 'x'; DELETE FROM customers; --' error: update or delete on table "customers" violates foreign key constraint "tickets_customer_fk" on table "tickets"PostgreSQL a exécuté le DELETE. Seule la clé étrangère de Contraintes d’intégrité l’a arrêté, car chaque client a des billets. L’instruction a échoué dans son ensemble, donc rien n’a été supprimé. C’était de la chance, pas une protection prévue : une table sans cette règle serait maintenant vide. N’essayez pas cette entrée sur une base de données qui compte pour vous.
L’idée : le SQL et les valeurs voyagent séparément
Section intitulée « L’idée : le SQL et les valeurs voyagent séparément »La version sûre sépare les deux éléments, comme à la banque. Le texte SQL est fixe et contient un espace réservé, %s. La valeur est envoyée séparément à la base de données :
def find_customer_safe(conn: psycopg.Connection, email: str) -> list[tuple]: sql = "SELECT customer_id, name, email FROM customers WHERE email = %s" print(f" SQL sent: {sql} value sent separately: {email!r}") return conn.execute(sql, (email,)).fetchall()Une valeur passée ainsi est un paramètre, et la requête est une requête paramétrée. Le nom vient des mathématiques : la requête est fixe, et le paramètre est la partie qui change entre les exécutions.
Pourquoi est-ce sûr? psycopg envoie le texte SQL et les valeurs dans des parties distinctes de son message au serveur. PostgreSQL lit d’abord le texte et détermine le sens de la requête : une table, une condition. Ensuite seulement, il utilise la valeur pour remplir l’espace réservé. Un guillemet dans la valeur est simplement un caractère dans une adresse courriel. Voici la même attaque, envoyée comme paramètre :
safe (parameter): SQL sent: SELECT customer_id, name, email FROM customers WHERE email = %s value sent separately: "x' OR '1'='1" 0 row(s)Aucun client n’a l’adresse courriel x' OR '1'='1, donc le résultat contient zéro ligne. C’est correct.
Description textuelle du diagramme
L’entrée est la même des deux côtés. Avec la construction par chaînes, l’entrée devient une partie du texte SQL. PostgreSQL lit donc une requête qu’Amira n’a jamais écrite : deux conditions, dont la deuxième est toujours vraie. Il retourne 40 lignes. Avec un paramètre, PostgreSQL lit d’abord le texte fixe et trouve une seule condition. La valeur arrive séparément et remplit l’espace réservé comme une adresse courriel. Aucun client n’a cette adresse, donc il retourne 0 ligne.
Les trois entrées, côte à côte :
- Non sûr
- Sûr
Description textuelle du graphique
Paires de barres pour trois entrées, non sûre puis sûre. L’adresse normale olga.olsen@example.com : 1 ligne non sûre, 1 ligne sûre. x' OR '1'='1 : 40 lignes non sûres (tous les clients), 0 sûre. x' UNION SELECT ticket_id, subject, body FROM tickets -- : 200 lignes non sûres (tous les billets), 0 sûre.
Ce que cela nous apprend. Pour une entrée normale, les deux versions donnent le même résultat. Un test avec des données normales ne peut pas voir la différence. Seule une entrée contenant un guillemet la révèle; les tests du projet envoient donc ces entrées exprès.
Deux limites méritent votre attention :
- Un paramètre est une valeur, jamais un nom. Vous ne pouvez pas passer un nom de table ou de colonne comme
%s. L’erreur courante ci-dessous montre le problème. Choisissez les noms dans une liste fixe dans votre code. - Certains caractères gardent un sens dans une valeur. Dans un motif
LIKE,%et_sont des caractères génériques. Un paramètre ne peut pas modifier la requête, mais la valeur%correspond quand même à toutes les lignes. L’exercice À vous de jouer le montre.
Larkfield : un fichier pour tout le SQL
Section intitulée « Larkfield : un fichier pour tout le SQL »Le service de soutien a maintenant besoin de trois points de terminaison pour les billets. Il faut une liste avec filtres, un billet avec ses messages et un nouveau message. Amira ne veut pas de SQL dans chaque route. Elle place toutes les instructions SQL de l’API dans un module, ticket_api/repository.py. Un dépôt est la couche entre les routes et la base de données. Une route lui demande « une page de billets de facturation ouverts », et le dépôt connaît le SQL.
Ses règles sont au début du fichier :
"""Every SQL statement of the API, in one place.
The rules (Module 3 of the databases course):- Values always go to the database as parameters (%s or %(name)s), never inside the SQL text. The SQL text is built only from fixed pieces in this file.- A function gets a connection; the caller decides where the transaction starts and ends."""La liste a des filtres facultatifs, donc sa clause WHERE change d’une requête HTTP à l’autre. Faut-il la construire par chaînes? Il faut la construire, mais seulement à partir d’éléments fixes :
def list_query(*, status=None, team=None, customer_id=None, limit=20, offset=0) -> tuple[str, dict]: where, params = [], {"limit": limit, "offset": offset} for column, value in (("status", status), ("team", team), ("customer_id", customer_id)): if value is not None: where.append(f"t.{column} = %({column})s") params[column] = value sql = ( "SELECT * FROM tickets t" + (" WHERE " + " AND ".join(where) if where else "") + " ORDER BY t.created_at DESC, t.ticket_id DESC LIMIT %(limit)s OFFSET %(offset)s" ) return sql, paramsLa chaîne f assemble ici seulement des noms de colonnes écrits dans le code : status, team, customer_id. La requête HTTP décide quels éléments fixes inclure, jamais ce qu’ils contiennent. Chaque valeur va dans params et voyage comme paramètre nommé, par exemple %(team)s.
Les routes ajoutent une deuxième couche. FastAPI vérifie chaque valeur avant que le dépôt la voie. team doit être une des cinq équipes connues, et l’identifiant d’un billet doit contenir T-, puis cinq ou six chiffres. Cette attaque n’atteint donc jamais la base de données :
$ curl -s "http://127.0.0.1:8000/v1/tickets?team=billing'%20OR%20'1'='1"{"error":{"code":"invalid_request","message":"The request is not valid.","request_id":"c9ecb95fcea8","fields":["team"]}}La réponse est 422. Quelle couche protège donc l’API? Les deux, pour des raisons différentes. La validation donne au client une erreur claire. Le paramètre est la garantie. Personne ne peut vérifier un champ de texte libre, comme le corps d’un message, à partir d’une liste. Pourtant, ce texte ne doit jamais devenir du SQL.
Le point de terminaison de liste a une autre propriété que ce cours abordera de nouveau. Pour chaque billet d’une page, il envoie deux autres requêtes SQL : le nom du client et la dernière exécution d’IA. Une page de 20 billets envoie 41 requêtes SQL. L’API indique ce nombre dans l’en-tête de réponse X-DB-Queries. C’est correct, mais lent. Le module 4 le mesure et le corrige.
Un ORM : la même règle, appliquée pour vous
Section intitulée « Un ORM : la même règle, appliquée pour vous »Beaucoup de projets Python n’écrivent pas le SQL à la main. Ils utilisent un ORM, un outil de correspondance objet-relationnel. Cette bibliothèque associe une table à une classe Python et écrit le SQL pour vous. SQLAlchemy est le plus courant en Python. Aurait-il arrêté l’injection?
L’auteur a associé trois tables avec SQLAlchemy 2.1.4 et chargé la même page de billets de facturation ouverts, hors du projet. Une recherche par nom de client montre le SQL envoyé par l’ORM :
SELECT customers.customer_id, customers.nameFROM customersWHERE customers.name = %(name_1)s::VARCHARLa valeur est un paramètre lié, %(name_1)s, jamais collé dans le texte. Un ORM suit donc la règle pour vous, tant que vous utilisez ses méthodes de requête. Sa solution de rechange, le texte SQL brut, présente le même risque que la première version d’Amira.
La même exécution montre le piège le plus connu des ORM. Le code le plus évident lit ticket.customer.name dans une boucle. Il a envoyé 41 requêtes SQL pour une page de 20 billets. L’ORM charge chaque ligne liée quand le code y accède pour la première fois. La même page avec une option, selectinload, a envoyé 3 requêtes SQL : la page, puis tous ses clients, puis toutes ses exécutions d’IA. Sur les grandes données avec les index du module 4, les temps étaient de 15,2 ms contre 2,2 ms. Sans ces index, comme dans votre base de données actuelle, les deux versions sont plus lentes, surtout celle avec 41 requêtes SQL.
| SQL direct avec psycopg (ce projet) | Un ORM comme SQLAlchemy | |
|---|---|---|
| Valeurs comme paramètres | Vous écrivez %s; la révision le vérifie |
Automatique avec les méthodes de requête |
| SQL exécuté | Exactement ce que vous avez écrit | Généré; consignez-le pour le voir |
| Nombre de requêtes SQL | Ce que vous avez écrit | Facile d’en envoyer beaucoup par accident (41 ici) |
| Lignes vers objets Python | À la main (dict_row, modèles Pydantic) |
Automatique |
| Coût | Plus de SQL à écrire et à réviser | Une grosse bibliothèque à apprendre et une couche de plus à déboguer |
Ce cours conserve le SQL direct. Chaque plan de requête et chaque verrou des modules 3 et 4 est plus facile à voir quand vous avez écrit le SQL. Un ORM est un bon choix pour une équipe qui le connaît. La règle sur les valeurs et l’habitude de compter les requêtes SQL restent les mêmes. Le script de comparaison est dans le dépôt des exercices, dans C20/optional/orm, avec son propre README et environnement virtuel. Il est facultatif.
Exercice guidé : le dépôt et les points de terminaison des billets
Section intitulée « Exercice guidé : le dépôt et les points de terminaison des billets »Vous ajoutez le dépôt, les trois points de terminaison des billets, la démonstration d’injection et leurs tests. Les points de terminaison obtiennent leurs connexions d’un nouveau module, ticket_api/db.py. Pour l’instant, vous désactivez son pool de connexions (pool). Chaque requête HTTP ouvre ainsi sa propre connexion, comme le faisait history.py jusqu’ici. La prochaine leçon explique le pool de connexions et l’active.
-
Ouvrez un terminal dans
~/projects/ticket-apiet activez l’environnement virtuel (source .venv/bin/activate; Windows :.venv\Scripts\Activate.ps1). -
Copiez les fichiers de cette leçon depuis l’instantané
end-of-m03. Les nouveaux fichiers sontrepository.py,tickets.py,db.py,injection_demo.pyet deux fichiers de tests. Les autres fichiers changent : modèles, erreurs, main, config, history, contrat, dépendances,pyproject.toml(version 1.3.0), étiquette de l’image danscompose.yamlet un fichier de tests.Fenêtre de terminal S=../nextia-learning-labs/C20/snapshots/end-of-m03for f in ticket_api/repository.py ticket_api/tickets.py ticket_api/db.py ticket_api/models.py \ticket_api/errors.py ticket_api/main.py ticket_api/config.py ticket_api/history.py \contract/openapi.json requirements.txt requirements-run.txt requirements-lock.txt \pyproject.toml compose.yaml scripts/injection_demo.py \tests/test_safe_queries.py tests/test_tickets_api.py tests/test_transactions.py; docp "$S/$f" "$f"doneFenêtre de terminal $S = "..\nextia-learning-labs\C20\snapshots\end-of-m03"$files = 'ticket_api\repository.py', 'ticket_api\tickets.py', 'ticket_api\db.py', 'ticket_api\models.py','ticket_api\errors.py', 'ticket_api\main.py', 'ticket_api\config.py', 'ticket_api\history.py','contract\openapi.json', 'requirements.txt', 'requirements-run.txt', 'requirements-lock.txt','pyproject.toml', 'compose.yaml', 'scripts\injection_demo.py','tests\test_safe_queries.py', 'tests\test_tickets_api.py', 'tests\test_transactions.py'foreach ($f in $files) { Copy-Item "$S\$f" $f }Si vous avez téléchargé le dépôt des exercices comme fichier ZIP, son dossier est
nextia-learning-labs-main. Modifiez la première ligne en conséquence. -
Installez le seul nouveau paquet,
psycopg-pool:python -m pip install -r requirements.txt. -
Ajoutez une ligne à
.env:DB_POOL=false. Elle désactive le pool de connexions jusqu’à la prochaine leçon. -
Exécutez les tests :
python -m pytest. Attendez-vous à67 passed(67 tests réussis), soit 14 de plus qu’avant. -
Exécutez la démonstration trois fois, avec les trois entrées :
Fenêtre de terminal python -m scripts.injection_demopython -m scripts.injection_demo "x' OR '1'='1"python -m scripts.injection_demo "x' UNION SELECT ticket_id, subject, body FROM tickets --"Comparez votre sortie aux sorties ci-dessus. Seules les lignes non sûres changent selon les entrées.
-
Démarrez l’API avec
fastapi dev. Dans un deuxième terminal, demandez deux billets de facturation ouverts :curl -s 'http://127.0.0.1:8000/v1/tickets?status=open&team=billing&limit=2'. Si votre.envdéfinitAPI_KEY, ajoutez-H "X-API-Key: <your key>". Sous Windows, tapezcurl.exeplutôt quecurl. -
Envoyez l’attaque comme filtre :
curl -s "http://127.0.0.1:8000/v1/tickets?team=billing'%20OR%20'1'='1". Arrêtez ensuite l’API avec Ctrl+C.
Afficher le résultat attendu
L’étape 5 se termine par 67 passed. Les nouveaux tests comprennent test_string_building_lets_text_change_the_query (40 lignes), test_a_parameter_is_only_a_value et test_the_api_treats_a_quote_as_text (422). L’étape 7 retourne deux billets, le plus récent en premier :
{"items":[{"ticket_id":"T-30197","customer_id":"C-0029","customer_name":"Noah Ramos","subject":"Payment failed but money taken","team":"billing","priority":2,"status":"open","created_at":"2026-10-08T14:47:53Z","message_count":0,"last_message_at":null,"last_run_model":"chat-small","last_run_status":"ok"},{"ticket_id":"T-30179","customer_id":"C-0033","customer_name":"Yusuf Okafor","subject":"Wrong price charged","team":"billing","priority":3,"status":"open","created_at":"2026-10-06T12:43:44Z","message_count":0,"last_message_at":null,"last_run_model":"keywords-1.1","last_run_status":"ok"}],"next_offset":2}L’étape 8 retourne l’erreur 422 ci-dessus, avec "fields":["team"]. Votre request_id est différent. Si une étape affiche connection refused (connexion refusée), PostgreSQL n’est pas en marche : exécutez docker compose up -d db et réessayez.
À vous de jouer : une recherche sûre dans les sujets
Section intitulée « À vous de jouer : une recherche sûre dans les sujets »Changez une chose : cherchez des mots dans les sujets des billets, sans distinguer les majuscules des minuscules. Écrivez scripts/search_subjects.py avec une fonction search_subjects(conn, words) qui utilise ILIKE et un paramètre. Ajoutez une fonction main() qui affiche le nombre de lignes. Faites d’abord une prédiction : que retournent les entrées charged twice (facturé deux fois), x' OR '1'='1 et % sur les petites données?
Afficher une solution et la réponse
"""Your turn: search ticket subjects for some words, safely."""
import sys
import psycopg
from ticket_api.config import load_env, read_secret
def search_subjects(conn: psycopg.Connection, words: str) -> list[tuple]: sql = "SELECT ticket_id, subject FROM tickets WHERE subject ILIKE %s ORDER BY ticket_id" return conn.execute(sql, (f"%{words}%",)).fetchall()
def main() -> int: load_env() with psycopg.connect(read_secret("DATABASE_URL")) as conn: rows = search_subjects(conn, sys.argv[1]) print(f"{len(rows)} row(s)" + (f", first: {rows[0]}" if rows else "")) return 0
if __name__ == "__main__": sys.exit(main())La chaîne f construit la valeur (%charged twice%), pas le texte SQL; elle est donc sûre. L’exécution de l’auteur :
$ python -m scripts.search_subjects "charged twice"6 row(s), first: ('T-30002', 'Charged twice for order LK-182074')$ python -m scripts.search_subjects "x' OR '1'='1"0 row(s)$ python -m scripts.search_subjects "%"200 row(s), first: ('T-30001', 'Tracking number does not work')L’attaque ne trouve rien. Mais % trouve les 200 billets, tout comme _. Ce n’est pas une injection, car la requête n’a pas changé. La valeur elle-même est un motif, et % correspond à n’importe quel texte. Si une recherche doit trouver ces caractères tels quels, placez une barre oblique inverse avant chacun : words.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_"). Avec cette ligne, % et _ trouvent 0 billet, et charged twice en trouve encore 6. Pour une recherche publique, limitez aussi le nombre de lignes, comme le point de terminaison de liste avec limit. Ce script vous appartient : l’instantané ne le contient pas.
Un nom de table venant de la requête HTTP
Section intitulée « Un nom de table venant de la requête HTTP »Erreur courante
Un paramètre utilisé comme nom de table
Symptôme : Sam écrit un petit script d’administration qui compte les lignes de n’importe quelle table. Il a appris la règle, donc il passe le nom de table comme paramètre :
conn.execute("SELECT count(*) FROM %s", ("tickets",))psycopg.errors.SyntaxError: syntax error at or near "$1"LINE 1: SELECT count(*) FROM $1 ^Cause : un paramètre est une valeur, et PostgreSQL doit connaître la table avant de pouvoir utiliser une valeur. psycopg a envoyé le texte avec un espace réservé numéroté, $1, là où le SQL n’autorise aucune valeur.
Correction :
- Si le nom vient d’une requête HTTP, vérifiez-le à partir d’une liste fixe dans votre code (
if table not in {"tickets", "messages"}: …), comme le faitlist_queryavec ses trois noms de colonnes. - Construisez ensuite le SQL avec
psycopg.sql, qui encadre un nom comme un nom :sql.SQL("SELECT count(*) FROM {}").format(sql.Identifier(table)). Cela donneSELECT count(*) FROM "tickets". - Ne revenez jamais à une chaîne f. Un nom entre guillemets échoue sans danger :
sql.Identifier("tickets; DROP TABLE tickets")donnerelation "tickets; DROP TABLE tickets" does not exist(la relation n’existe pas), et rien n’est supprimé.
Pour un service d’IA : le texte d’un modèle est aussi du texte externe. Un modèle peut écrire une partie d’une requête SQL, comme une valeur de filtre ou une colonne de tri. Traitez ce texte comme une requête HTTP : les valeurs comme paramètres, les noms tirés d’une liste fixe.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
L'API valide chaque filtre avec une liste de valeurs connues. Le paramètre dans le dépôt reste-t-il nécessaire? Pourquoi?
Afficher la réponse
Oui. La validation protège seulement les champs qui ont une liste fixe de valeurs, comme team (équipe) et status (état). Le texte libre, comme le corps d’un message ou un mot recherché, ne peut pas être vérifié avec une liste. Il ne doit pourtant jamais devenir du SQL. Un paramètre garantit cela pour chaque valeur, même dans le prochain point de terminaison ajouté sans validation. La validation donne un code 422 clair; le paramètre apporte la garantie.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédisez le résultat : un vrai client a l'adresse courriel o'brien@example.com, qui contient une apostrophe. Que font les recherches sûre et non sûre avec cette adresse?
Afficher la réponse
La recherche sûre cherche cette adresse exacte. L’apostrophe est un simple caractère. Elle trouve donc le client s’il existe (dans le petit jeu de données, 0 lignes). La recherche non sûre insère l’adresse dans '...'. L’apostrophe termine la chaîne trop tôt, et PostgreSQL lit brien@example.com' comme du SQL. L’exécution de l’auteur a affiché error: syntax error at or near "brien" (erreur de syntaxe à « brien » ou près de ce mot). Construire des chaînes pose donc aussi problème aux vrais clients, pas seulement aux attaquants. Des erreurs comme celle-ci permettent souvent de découvrir des failles d’injection.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Un ORM écrit toutes les requêtes d'un projet avec des paramètres liés. Sa page de liste est lente. Que devriez-vous vérifier d'abord, et pourquoi?
Afficher la réponse
Comptez les requêtes envoyées par une seule page. L’ORM protège les valeurs, mais charge les lignes liées quand le code y accède pour la première fois. Une boucle sur 20 billets de soutien peut donc envoyer 41 requêtes, comme dans la comparaison. Consignez le SQL (SQLAlchemy peut afficher chaque instruction) ou comptez les requêtes, comme le projet avec son en-tête X-DB-Queries. Chargez ensuite les lignes liées avec une requête par table, par exemple avec selectinload.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Construire des chaînes permet au texte de modifier le SQL :
x' OR '1'='1a retourné les 40 clients. UnUNION(union) a retourné les 200 billets, et unDELETE(suppression) s’est exécuté jusqu’au blocage par une clé étrangère. - Une requête paramétrée envoie séparément le texte SQL et les valeurs. PostgreSQL lit le texte d’abord. Une valeur peut donc seulement remplir son emplacement : les mêmes attaques ont retourné 0 lignes.
- Un paramètre est une valeur, jamais un nom. Construisez les noms uniquement à partir d’éléments fixes dans votre code, avec
psycopg.sql.Identifierau besoin. Surveillez%et_dans les valeurs deLIKE(correspondance de motif). - Le dépôt (
ticket_api/repository.py) contient toutes les instructions SQL de l’API. Les routes valident d’abord; le dépôt transmet chaque valeur comme paramètre. - Un ORM écrit les paramètres liés pour vous, mais peut cacher de nombreuses requêtes : 41 pour une page, contre 3 avec
selectinload. - Votre projet possède maintenant les points de terminaison des billets, avec une nouvelle connexion par requête.
python -m pytestaffiche67 passed(67 tests réussis).
Leçon suivante : Gestion des connexions