U6-S2-M6Application web et base MySQL
0 / 0
Scénario 2 · TechnoVert

Application web et base MySQL

Tout ce dont vous avez besoin pour ce module, dans un seul document. Cochez au fur et à mesure.

U6-S2-M6module
Élèvedocument
TechnoVertscénario 2
B validé · 3 h 45format et durée

Comment utiliser ce document

  • À gauche, une partie à la fois. Cliquez pour changer de partie.
  • Les boutons bleus A1 ouvrent une annexe dans le panneau de droite.
  • Vos réponses et vos cases cochées sont gardées dans ce navigateur.
Rappels

Ce qu'il faut savoir avant de commencer

Ce qu'il faut savoir avant de commencer. Lisez-le si vous avez un doute. Environ 10 minutes.

C'est quoi une requête SQL ?

Une base de données range les données dans des tables. Chaque table a des colonnes et des lignes, comme un tableur.

On parle à la base en SQL. SELECT lit des lignes, WHERE les filtre, INSERT en ajoute.

SELECT nom, statut FROM chantier WHERE statut = 'ouvert';
INSERT INTO client (nom, ville) VALUES ('Mairie de Lusse', 'Lusse');

En SQL, un texte s'écrit entre apostrophes : 'ouvert'. Un nombre s'écrit sans apostrophes.

C'est quoi une clé étrangère ?

Une colonne qui contient le numéro d'une ligne d'une autre table. Dans la table chantier, la colonne id_client contient le numéro du client.

De la même façon, dans la table intervention, la colonne id_chantier contient le numéro du chantier. C'est elle qui relie une intervention à son chantier.

Pour afficher le nom du client avec le chantier, on relie les deux tables avec JOIN :

SELECT chantier.nom, client.nom FROM chantier JOIN client ON client.id = chantier.id_client;

C'est quoi un tuple en Python ?

Un tuple, c'est des valeurs rangées entre parenthèses : (3, "atelier"). On s'en sert pour passer les valeurs à une requête.

Pour une seule valeur, on écrit une virgule à la fin : (nom,). Sans elle, ce n'est pas un tuple.

Comment marche une page Flask ?

Flask est une boîte à outils Python pour faire un site web. Une route relie une adresse à une fonction Python, appelée vue.

La vue prépare les données, puis remplit un gabarit : une page HTML avec des trous. On lance le site avec flask --app app run.

Comment lancer MySQL sur votre poste ?

Sur votre poste, MySQL tourne grâce à XAMPP. On le pilote depuis le panneau de contrôle de XAMPP :

On parle ensuite à la base dans un terminal, avec la commande mysql -u root.

Vérifiez-vous

Voir les réponses
  1. SELECT nom FROM chantier WHERE statut = 'cloture';
  2. id_chantier, qui contient le numéro du chantier.
  3. Dans le panneau de contrôle de XAMPP : « MySQL » doit être en vert. Sinon, cliquez sur « Start ».
Cours

Brancher l'application sur la base

COURS-U6-S2-06

L'application de suivi des chantiers de TechnoVert marche. Les techniciens saisissent leurs interventions chaque soir. Mais les données sont dans un simple fichier, sur le serveur web.

Hélène Vasseur, la dirigeante, veut qu'elles passent dans la base MySQL de l'entreprise. Elle a une question : « Et si quelqu'un tape n'importe quoi dans une page, il peut abîmer la base ? »

La réponse est oui, si l'application est mal écrite. À la fin de la séance, vous saurez brancher l'application sur la base sans lui ouvrir cette porte.

Le plan de la séance

  1. Parler à la base depuis Python.
  2. L'injection SQL et la requête paramétrée.
  3. Une couche d'accès aux données.
  4. Les identifiants hors du code.
  5. Un jeu de données qu'on peut rejouer.

La question de départ

Sur un site, vous tapez une apostrophe dans un champ de recherche. La page affiche une erreur de base de données. Comment un seul caractère peut-il faire planter le site ?

Capsule 1 sur 5Parler à la base depuis Python

Commençons par le branchement lui-même. Comment un programme Python envoie-t-il une requête SQL à MySQL ?

Il passe par un CONNECTEUR : une bibliothèque Python qui sait parler à MySQL. Ici, c'est mysql.connector. Le connecteur ouvre une connexion, puis on lui confie un CURSEUR : l'objet qui envoie les requêtes et ramène les lignes.

Pensez à un appel téléphonique. On compose le numéro, on parle, on écoute la réponse, on raccroche.

Le connecteur, c'est le téléphone. Le curseur, c'est la conversation.

  1. Ouvrir la connexion : l'adresse du serveur, le compte, le mot de passe, la base.
  2. Créer un curseur.
  3. Exécuter la requête avec execute().
  4. Lire les lignes avec fetchall(). Pour une écriture, valider avec commit().
  5. Fermer la connexion avec close().
import mysql.connector

connexion = mysql.connector.connect(host="127.0.0.1", user="tv_appli",
                                    password="…", database="technovert")
curseur = connexion.cursor(dictionary=True)
curseur.execute("SELECT id, nom, statut FROM chantier")
chantiers = curseur.fetchall()     # une liste de dictionnaires
connexion.close()

Avec dictionary=True, chaque ligne arrive sous forme de dictionnaire : {"id": 3, "nom": "Éclairage des allées", ...}. C'est la forme qu'attendent les gabarits.

Application Python (Flask) ouvre, exécute, lit, ferme Connecteur mysql.connector Serveur MySQL base technovert tables client, chantier, intervention requête les lignes reviennent Ouvrir · curseur · execute() · fetchall() ou commit() · close()
L'application passe par le connecteur pour parler au serveur MySQL, puis ferme la connexion

On le fait ensemble — ajouter une intervention

1. Quelle est la première chose à faire ?

2. Quelle requête le curseur exécute-t-il ?

3. Faut-il fetchall() ?

4. Que faut-il ajouter avant de fermer ?

Exemple Pourquoi
✔ Ouvrir, exécuter, lire, fermer, dans la même fonction La connexion ne reste pas ouverte pour rien.
✘ Un INSERT sans commit() Aucune erreur ne s'affiche, mais la ligne n'est jamais enregistrée.
✘ Oublier close() dans une page très visitée Les connexions s'accumulent. MySQL finit par refuser les nouvelles.

Checkpoint

À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.

1. Quel objet envoie la requête et ramène les lignes ?

2. Un UPDATE qui clôture un chantier marche, mais le changement disparaît. Qu'est-ce qui manque ?

Capsule 2 sur 5L'injection SQL et la requête paramétrée

On sait maintenant envoyer une requête. Souvent, elle contient une valeur tapée par l'utilisateur : un service, un nom. Comment l'y mettre ?

La mauvaise façon, c'est de coller la saisie dans le texte de la requête. L'utilisateur peut alors taper un morceau de SQL. C'est l'INJECTION SQL : la saisie devient une partie de l'ordre donné à la base.

C'est comme un bon de commande où le client remplit la case « quantité ». S'il écrit « 2, et offrez-moi le reste du magasin », et que personne ne relit, le magasinier exécute tout.

  1. Le code colle la saisie dans la requête, entre apostrophes.
  2. L'utilisateur tape une apostrophe : elle ferme le texte trop tôt.
  3. La suite de la saisie devient du SQL, que la base exécute.
  4. La base fait ce que l'utilisateur a écrit, pas ce que le développeur voulait.
Code :    "... WHERE technicien = '" + login + "' AND service = '" + service + "'"
Saisie :  x' OR '1'='1
Requête : ... WHERE technicien = 'ngarcia' AND service = 'x' OR '1'='1'

'1'='1' est toujours vrai. La base renvoie les interventions de tous les techniciens.

La parade s'appelle la REQUÊTE PARAMÉTRÉE. On écrit %s à la place de chaque valeur.

On donne les valeurs à part, dans un tuple. Le connecteur les envoie comme des données, jamais comme du SQL.

curseur.execute("SELECT ... WHERE technicien = %s AND service = %s", (login, service))

Avec la même saisie, la base cherche un service qui s'appelle vraiment x' OR '1'='1. Elle n'en trouve aucun.

Saisie collée : elle devient du SQL "... WHERE service = '" + service + "'" service = x' OR '1'='1 ... WHERE service = 'x' OR '1'='1' '1'='1' est toujours vrai → la base renvoie tout Saisie en paramètre : elle reste une donnée execute("... WHERE service = %s", (service,)) service = x' OR '1'='1 → cherché tel quel, aucune ligne
La saisie collée devient du SQL ; la saisie passée en paramètre reste une donnée

On le fait ensemble — la recherche d'un chantier par nom

1. Le code écrit "... WHERE nom = '" + nom + "'". Est-il vulnérable ?

2. Par quoi remplace-t-on '" + nom + "' ?

3. Comment donne-t-on la valeur ?

4. Pourquoi une virgule dans (nom,) ?

Exemple Pourquoi
✔ execute("... WHERE id = %s", (numero,)) La valeur part à part : elle ne peut pas changer la requête.
✘ execute(f"... WHERE service = '{service}'") Une f-string colle la saisie, exactement comme +.
✘ "... WHERE service = '%s'" avec des apostrophes Le connecteur ajoute les siennes. Le texte obtenu est faux.

Instant histoire — 2015 : TalkTalk et ses vieilles pages

En 2015, TalkTalk est un grand opérateur de téléphone et d'Internet au Royaume-Uni.

Des pirates, dont des adolescents, trouvent d'anciennes pages de son site qui collent la saisie dans leurs requêtes.

Par injection SQL, ils volent les données d'environ 157 000 clients. L'entreprise reçoit une amende record de 400 000 livres.

Qu'est-ce qui aurait évité ça ?

Checkpoint

À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.

1. Réécrivez "SELECT * FROM client WHERE ville = '" + ville + "'" en requête paramétrée.

2. Un collègue dit : « Mon menu déroulant ne propose que trois services, pas de risque. » A-t-il raison ?

Une règle de droit, enfin. Tester une injection sur un site qui n'est pas le vôtre, sans autorisation écrite, est un délit. On ne s'entraîne que sur ses propres machines.

Capsule 3 sur 5Une couche d'accès aux données

La requête est sûre. Reste à savoir où ranger tout ce SQL. Dans chaque vue, au milieu du code de la page ?

Non. On le range dans une COUCHE D'ACCÈS AUX DONNÉES : un module Python à part, qui contient toutes les requêtes.

Les vues appellent ses fonctions, comme lire_chantiers(). Elles ne voient jamais de SQL.

Au restaurant, le serveur ne va pas en cuisine. Il passe la commande au passe-plat, et le plat revient. Si la cuisine change de four, la salle ne voit aucune différence.

  1. Toutes les requêtes vont dans un seul fichier, par exemple acces_bdd.py.
  2. Chaque besoin devient une fonction au nom clair : lire_chantier(numero).
  3. Les vues appellent ces fonctions et reçoivent des listes ou des dictionnaires.
  4. La couche traduit les pannes : si MySQL ne répond pas, elle lève une erreur à elle, BaseIndisponible.
  5. L'application répond proprement à cette erreur : une page « Service indisponible », code 503.

Le code 503 veut dire : le serveur marche, mais il attend un autre service qui ne répond pas. L'utilisateur voit un message clair, pas une trace.

Les vues liste_chantiers() saisir() aucun SQL ici Couche d'accès acces_bdd.py tout le SQL %s partout BaseIndisponible MySQL base technovert appelle SQL Pour changer de base, on touche la couche seule. Les vues ne bougent pas.
Les vues appellent la couche d'accès ; seule la couche parle à MySQL

On le fait ensemble — la base s'arrête pendant la nuit

1. Où l'erreur de connexion apparaît-elle en premier ?

2. Que fait ouvrir() de cette erreur ?

3. Que voit le technicien qui ouvre la liste ?

4. Où le technicien de maintenance lit-il la vraie cause ?

Exemple Pourquoi
✔ On passe du fichier à MySQL en changeant une ligne d'import Les vues n'ont pas bougé : c'est la preuve que la couche est bien isolée.
✘ Un SELECT écrit dans la vue liste_chantiers() Le SQL se disperse. Pour vérifier les injections, il faut relire tout le site.
✘ La trace MySQL affichée à l'utilisateur quand la base tombe Elle montre le nom du serveur et du compte. Et l'utilisateur ne sait pas quoi faire.

Checkpoint

À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.

1. Vous cherchez toutes les requêtes de l'application pour les vérifier. Où regardez-vous ?

2. Que veut dire le code 503 ?

Capsule 4 sur 5Les identifiants hors du code

La couche d'accès se connecte avec un compte et un mot de passe. Où les écrire ?

Surtout pas dans le code. Le code se copie, s'envoie, se range dans un dépôt partagé. Le mot de passe partirait avec.

On le met dans une VARIABLE D'ENVIRONNEMENT : une valeur que le système donne au programme quand il le lance.

C'est comme le code du portail d'une résidence. On ne le grave pas sur le portail. Chaque habitant le connaît, et on le change sans changer le portail.

  1. On écrit les identifiants dans un fichier d'environnement, hors du dossier du code.
  2. On le protège : on le range dans son dossier personnel, que les autres comptes de la machine ne lisent pas. Sur un serveur Linux, on ajoute chmod 600.
  3. L'application le charge au démarrage, avec load_dotenv().
  4. Le code les lit avec os.environ.
Fichier technovert.env (dossier perso)  Code de acces_bdd.py
TV_BDD_UTILISATEUR=tv_appli           user=os.environ["TV_BDD_UTILISATEUR"]
TV_BDD_MOT_DE_PASSE=…                 password=os.environ["TV_BDD_MOT_DE_PASSE"]

Autre gain : le même code tourne sur votre poste de test et sur le serveur de production. Seul le fichier d'environnement change.

Le même code os.environ["TV_BDD_MOT_DE_PASSE"] Poste de test technovert.env mot de passe de test Serveur de production technovert.env vrai mot de passe lit ses identifiants dans le fichier d'environnement
Le code reste le même ; seul le fichier d'environnement change entre le test et la production

On le fait ensemble — la clé secrète de la session

1. Le code contient app.secret_key = "cle-de-labo". Est-ce un secret ?

2. Où la mettre ?

3. Qu'écrit-on dans le code ?

Exemple Pourquoi
✔ Un fichier d'exemple livré avec le code, avec a-remplacer comme valeur Il montre les noms attendus, sans aucun vrai secret.
✘ Le fichier d'environnement rangé dans le dossier du code Il part dans le ZIP ou dans le dépôt, avec le code.
✘ Le mot de passe écrit en commentaire, « pour mémoire » Un commentaire se lit aussi bien qu'une ligne de code.

Checkpoint

À l'oral, 3 minutes. Chacun note sa réponse sur sa feuille, puis on corrige.

1. On passe de votre poste de test au serveur de production. Que modifie-t-on ?

2. Sur le serveur Linux de production, que fait chmod 600 sur le fichier d'environnement ?

Capsule 5 sur 5Un jeu de données qu'on peut rejouer

Dernier outil, pour tester sans crainte. Pendant un test, on ajoute des lignes, on en casse. Comment revenir à un état connu ?

On garde un jeu de données de test : un script SQL qui efface la base et la recrée, toujours à l'identique. On le rejoue avant chaque série de tests. Un script de vérification compte ensuite les lignes et teste les cas connus.

mysql -u root -e "source technovert.sql"    # la base revient à l'état connu
python verifier_base.py                     # chaque ligne doit dire OK

Le même couple sert à prouver une restauration. On restaure la base sur une machine neuve, puis on lance la vérification. Si tout est OK, la restauration a marché.

Ce qu'on retient

Si vous ne devez retenir que trois choses

1. Une valeur saisie ne se colle jamais dans une requête : on écrit %s et on passe la valeur à part.

2. Tout le SQL vit dans la couche d'accès. Les vues appellent des fonctions, et une base arrêtée donne une page 503 propre.

3. Les identifiants vivent dans un fichier d'environnement protégé, jamais dans le code.

Revenons à Hélène Vasseur. Oui, quelqu'un peut taper n'importe quoi dans une page.

Avec des requêtes paramétrées, sa saisie reste une simple donnée, et la base ne l'exécute pas. Et le mot de passe de la base n'est écrit nulle part dans le code.

Fiche

Brancher l'application sur la base

COURS-U6-S2-06

L'essentiel

Les mots à connaître

Mot En clair
Connecteur La bibliothèque qui permet à Python de parler à MySQL.
Curseur L'objet qui envoie une requête et ramène les lignes.
Injection SQL Une saisie qui change la requête envoyée à la base.
Requête paramétrée Une requête avec %s, dont les valeurs sont envoyées à part.
Couche d'accès Le module qui contient toutes les requêtes de l'application.
Variable d'environnement Une valeur donnée au programme par le système, hors du code.
Bibliothèque, module Un fichier de code déjà écrit qu'on réutilise. Le connecteur est une bibliothèque ; la couche d'accès est un module.
127.0.0.1 L'adresse de la machine elle-même. La base tourne sur la même machine que l'application.
chmod 600 Sur un serveur Linux, rend un fichier lisible par son seul propriétaire. On l'utilise pour le fichier d'environnement.
f-string Un texte Python écrit f"...{x}...", où {x} est remplacé par la valeur. À ne jamais utiliser pour une requête SQL.

La méthode : écrire une fonction de la couche d'accès

  1. Ouvrez la connexion avec ouvrir().
  2. Écrivez la requête avec un %s par valeur, sans apostrophes autour.
  3. Exécutez avec les valeurs dans un tuple : (valeur,).
  4. Lisez avec fetchall(), ou validez avec commit().
  5. Fermez la connexion, puis renvoyez le résultat.

Un exemple concret

Lire les interventions d'un service, sans risque d'injection.

def interventions_du_service(service):
    connexion = ouvrir()
    curseur = connexion.cursor(dictionary=True)
    curseur.execute("SELECT * FROM intervention WHERE service = %s", (service,))
    lignes = curseur.fetchall()
    connexion.close()
    return lignes
Saisie Ce que la base reçoit Résultat
atelier le service atelier les interventions de l'atelier
x' OR '1'='1 un service nommé x' OR '1'='1 aucune ligne

Conclusion : la saisie piégée est traitée comme une simple donnée.

Les pièges

Labo

Injection, correction, restauration

LABO-U6-S2-06

Contexte — Vous reprenez l'application de suivi des chantiers de TechnoVert. Elle marche, mais elle range ses données dans un fichier. Hélène Vasseur veut qu'elles passent dans la base MySQL de l'entreprise.

Un collègue a repris une vieille page écrite par l'ancien prestataire. Elle colle la saisie dans sa requête. Vous allez le prouver, puis corriger.

Règle IA de la séance

Scénario 2 : l'IA peut vous expliquer une notion ou un message d'erreur. Elle n'écrit pas votre code.

Si vous lui posez une question, dites-la au formateur au point de contrôle suivant.

À la fin de ce labo, vous saurez brancher une application sur MySQL, isoler l'accès aux données, corriger une injection SQL et prouver une restauration.

Comment ça se valide — à chaque point de contrôle, vous montrez votre résultat au formateur. Il valide, puis vous passez à la suite.

Les mots de la séance

Mot Ce que ça veut dire
Connecteur La bibliothèque mysql.connector qui relie Python à MySQL.
Curseur L'objet qui envoie une requête et ramène les lignes.
Injection SQL Une saisie qui change la requête envoyée à la base.
Requête paramétrée Une requête avec %s, dont les valeurs sont passées à part.
Couche d'accès Le module acces_bdd.py qui contient toutes les requêtes.
Restaurer Recréer la base à l'identique à partir d'un fichier d'export.

Ce dont vous avez besoin

En binôme — le pilote tape, le lecteur lit les consignes et les annexes à voix haute, et vérifie chaque résultat. On inverse à la partie C.

Le plan du labo

Partie Étapes Ce que vous obtenez Temps
A — Brancher l'application 1 à 6 l'application qui lit la base MySQL 35 min
B — Isoler et sortir les secrets 7 à 11 une couche d'accès propre, sans mot de passe dans le code 25 min
C — Injecter puis corriger 12 à 17 une injection prouvée, puis rendue inoffensive 35 min
D — Exporter et restaurer 18 à 22 une base restaurée sur une pile vierge, vérifiée 25 min

PARTIE A — Brancher l'application

Étapes 1 à 6, environ 35 min. À la fin, l'application affiche les chantiers lus dans MySQL.

Besoin d'un rappel ? Revoir le cours : « Parler à la base depuis Python ».

01

Installer l'application et lancer MySQL

Vous devez voir : « MySQL » en vert dans le panneau de XAMPP. Dans VS Code, le dossier contient app.py, acces_bdd.py et les fichiers A2_technovert.sql, A3_compte_appli.sql, A6_verifier_base.py.

02

Créer la base et son compte

mysql --version

Si vous voyez « mysql n'est pas reconnu » : suivez l'annexe , « Rendre la commande mysql disponible », puis rouvrez VS Code.

mysql -u root -e "source A2_technovert.sql"
mysql -u root -e "source A3_compte_appli.sql"
mysql -u root -e "SELECT COUNT(*) FROM technovert.chantier;"

Vous devez voir : aucun message pour les deux premières commandes, puis un petit tableau avec le nombre 6.

03

Écrire la connexion et la lecture des chantiers

La couche d'accès est le fichier acces_bdd.py. Ses premières lignes, déjà écrites, chargent vos identifiants avec load_dotenv. Deux fonctions sont à écrire.

    try:
        return mysql.connector.connect(
            host=os.environ["TV_BDD_HOTE"],
            user=os.environ["TV_BDD_UTILISATEUR"],
            password=os.environ["TV_BDD_MOT_DE_PASSE"],
            database=os.environ["TV_BDD_BASE"],
            connection_timeout=3)
    except mysql.connector.Error as erreur:
        raise BaseIndisponible(str(erreur)) from erreur
    connexion = ouvrir()
    curseur = connexion.cursor(dictionary=True)
    curseur.execute(COLONNES_CHANTIER + "ORDER BY ch.date_debut")
    lignes = curseur.fetchall()
    connexion.close()
    return lignes

Le + colle ici deux textes fixes, écrits par vous. Ce n'est pas une saisie d'utilisateur : il n'y a donc aucun risque d'injection.

Vous devez avoir : un fichier enregistré avec Ctrl+S, et le nom d'une table relié par le JOIN. On teste le tout à l'étape 5.

Pourquoi ? Pourquoi le SELECT relie-t-il deux tables au lieu de lire la seule table chantier ?

Votre réponse :

04

Remplir le fichier d'environnement

Le fichier d'environnement se range dans votre dossier personnel, jamais dans le dossier du code. L'application le lit seule au démarrage.

Copy-Item technovert.env.exemple $HOME\technovert.env
Get-Content $HOME\technovert.env

Vous devez voir : quatre lignes de commentaire, puis vos cinq lignes TV_…, sans aucun a-remplacer. Les accents des commentaires peuvent mal s'afficher : ce n'est pas grave.

05

Basculer l'application sur la base

L'application importe encore l'ancienne couche, celle du fichier. On la fait pointer sur la base.

import acces_bdd as donnees
app.secret_key = os.environ["TV_CLE_SECRETE"]
python -m flask --app app run --debug

Vous devez voir : les six chantiers, comme avant. Cette fois, ils viennent de MySQL.

Si vous voyez la page « Service momentanément indisponible » : MySQL est arrêté dans XAMPP, ou le mot de passe de technovert.env est faux. Corrigez, puis rechargez la page.

06

Vérifier que les données viennent de la base

mysql -u root -e "UPDATE technovert.chantier SET nom='Essai base' WHERE id=1;"

Vous devez voir : le premier chantier s'appelle maintenant « Essai base ». L'application lit bien la base en direct.

POINT DE CONTRÔLE A

  • Montrez au formateur la liste avec « Essai base », et votre réponse à l'étape 3.
  • Le formateur a validé. Passez à la partie B.

PARTIE B — Isoler et sortir les secrets

Étapes 7 à 11, environ 25 min. À la fin, tout le SQL est dans la couche d'accès, et aucun mot de passe n'est dans le code.

Besoin d'un rappel ? Revoir le cours : « Une couche d'accès aux données ».

07

Vérifier que les vues ne contiennent pas de SQL

Select-String -Pattern "SELECT", "INSERT", "mysql" -Path app.py

Vous devez voir : aucune ligne. Tout le SQL est dans acces_bdd.py.

Pourquoi ? Pourquoi ranger toutes les requêtes dans un seul fichier, au lieu d'en mettre dans chaque vue ?

Votre réponse :

08

Provoquer une panne de base

Vous devez voir : la page « Service momentanément indisponible », pas une trace d'erreur.

09

Lire la vraie cause dans le journal

Vous devez voir : une ligne qui contient ERROR in app: Base indisponible, suivie de la cause. La ligne juste en dessous se termine par 503 -.

Vous devez voir : la liste des chantiers revient.

10

Trouver dans la doc

Vous devez avoir : un symbole, et le mot « tuple ». Ils servent en partie C.

11

Vérifier qu'aucun secret n'est dans le code

Select-String -Pattern "Labo-Appli", "password=" -Path app.py, acces_bdd.py

Vous devez voir : la seule ligne trouvée est password=os.environ[...]. Aucun mot de passe en clair.

POINT DE CONTRÔLE B

  • Montrez au formateur la page 503, la ligne du journal, et vos réponses aux étapes 7 et 10.
  • Le formateur a validé. Passez à la partie C.

PARTIE C — Injecter puis corriger

Étapes 12 à 17, environ 35 min. À la fin, une injection est prouvée, puis rendue inoffensive.

Besoin d'un rappel ? Revoir le cours : « L'injection SQL et la requête paramétrée ».

Changez de rôle : le lecteur devient pilote.

Cadre légal. Vous testez une injection sur votre propre machine, dans un but d'apprentissage. Le faire sur un site qui n'est pas le vôtre, sans autorisation écrite, est un délit.

12

Lire la page vulnérable

Un collègue a repris la page « Mes interventions » de l'ancien prestataire. Sa fonction est interventions_du_technicien, en bas de acces_bdd.py.

Vous devez avoir : les valeurs collées dans le texte avec des + et des apostrophes.

13

Voir la page normale

python -m flask --app app run --debug

Vous devez voir : la phrase 4 intervention(s) pour le service « atelier »., et un tableau de 4 lignes, toutes de ngarcia.

14

Prouver l'injection

http://127.0.0.1:5000/mes-interventions?service=x' OR '1'='1

Vous devez voir : un nombre d'interventions bien plus grand que 4, et des interventions d'autres techniciens. Vous voyez des données qui ne sont pas les vôtres.

Pourquoi ? Pourquoi la base a-t-elle renvoyé les interventions de tout le monde ?

Votre réponse :

15

Corriger avec une requête paramétrée

    requete = ("SELECT i.date_intervention, c.nom AS chantier, i.technicien, i.service, "
               "i.duree_heures FROM intervention i JOIN chantier c ON c.id = i.id_chantier "
               "WHERE i.technicien = %s AND i.service = %s "
               "ORDER BY i.date_intervention")
    curseur.execute(requete, (login, service))

Vous devez voir : le fichier s'enregistre sans erreur. On teste l'effet à l'étape 16.

16

Vérifier que l'attaque échoue

Vous devez voir : les 4 interventions de ngarcia, comme avant. La correction n'a rien cassé.

Vous devez voir : « 0 intervention(s) ». La base a cherché un service qui s'appelle vraiment x' OR '1'='1. Il n'existe pas.

17

Garder la preuve

Vous avez déjà la capture « avant correction », faite à l'étape 14.

Vous devez avoir : deux captures, l'une de l'étape 14, l'autre d'ici. Vous les montrez au point de contrôle C.

POINT DE CONTRÔLE C

  • Montrez au formateur les deux preuves et votre réponse à l'étape 14.
  • Le formateur a validé. Passez à la partie D.

PARTIE D — Exporter et restaurer

Étapes 18 à 22, environ 25 min. À la fin, la base est restaurée sur une pile vierge, et vérifiée.

Besoin d'un rappel ? Revoir le cours : « Un jeu de données qu'on peut rejouer ».

Le jeu de test de l'annexe recrée une base neuve, toujours la même. Un export, lui, sauvegarde la vraie base, avec les saisies des utilisateurs. C'est cet export qu'on restaure quand une machine tombe en panne. On le prouve ici sur une base neuve.

18

Exporter la base

mysqldump -u root technovert --result-file=export_technovert.sql
Select-String -Pattern "CREATE TABLE", "INSERT INTO" -Path export_technovert.sql

Vous devez voir : des lignes CREATE TABLE pour les quatre tables, et des lignes INSERT INTO. L'export contient la structure et les données.

19

Simuler une pile vierge

Une pile vierge, c'est une base qui n'existe pas encore. On efface la base pour le simuler.

mysql -u root -e "DROP DATABASE technovert;"

Vous devez voir : la page « Service momentanément indisponible ». La base n'existe plus.

20

Restaurer

mysql -u root -e "CREATE DATABASE technovert;"
mysql -u root technovert -e "source export_technovert.sql"

Vous devez voir : les six chantiers reviennent.

21

Vérifier la restauration avec le script

Une page qui s'affiche ne prouve pas que tout est là. Le script de l'annexe compte les lignes et teste les cas connus.

python A6_verifier_base.py

Vous devez voir : chaque ligne commence par OK, et la dernière ligne dit « 13 vérification(s) sur 13 réussie(s). ».

Si vous voyez un ÉCHEC : la restauration est incomplète, ou le jeu de test n'est pas le bon. Reprenez à l'étape 20.

22

Trouver dans la doc et conclure

Vous devez avoir : un service piégé, avec des apostrophes, et zéro ligne attendue.

Pourquoi ? Pourquoi vérifier une injection dans un script de recette, en plus de l'avoir corrigée ?

Votre réponse :

POINT DE CONTRÔLE D

  • Montrez au formateur le script qui affiche 13 sur 13, et vos réponses aux étapes 22.
  • Le formateur a validé. Le labo est fini.

POUR ALLER PLUS LOIN

10 à 15 min, si vous avez fini avant la fin de la séance.

Une IA a proposé cette fonction pour chercher un client par sa ville. Elle marche, mais elle contient trois défauts.

def clients_par_ville(ville):
    connexion = mysql.connector.connect(host="127.0.0.1", user="root",
                                        password="root", database="technovert")
    curseur = connexion.cursor(dictionary=True)
    curseur.execute(f"SELECT * FROM client WHERE ville = '{ville}'")
    return curseur.fetchall()
Ligne Le défaut Le danger

CE QU'IL FAUT RETENIR

  1. Une valeur saisie ne se colle jamais dans une requête : on écrit %s et on passe la valeur à part.
  2. Tout le SQL vit dans la couche d'accès. Une base arrêtée donne une page 503 propre, jamais une trace.
  3. Une restauration ne se croit pas : elle se prouve avec un script de vérification.
Mise en situation

Un tableau de bord qui tient

MISE EN SITUATION-U6-S2-M6

Contexte — Karim Benali veut un tableau de bord pour suivre les interventions de l'atelier. Il sera exposé aux utilisateurs de TechnoVert.

Hélène Vasseur a une exigence : « Je ne veux pas qu'un petit malin casse la base en tapant dans une case. » Le formateur jouera cet utilisateur malveillant.

Règle IA de la séance

Scénario 2 : l'IA peut vous expliquer une notion ou un message d'erreur. Elle n'écrit ni votre requête ni votre vue.

Si vous avez posé une question à une IA, dites-le pendant l'entretien. C'est pris en compte, pas reproché.

COMMENT VOUS SEREZ NOTÉ

Il n'y a aucun fichier à rendre. Vous êtes noté à l'oral, sur votre poste, pendant un entretien de 5 minutes avec le formateur. Il vient vous voir dès que votre tableau marche.

Pendant l'entretien, le formateur :

  1. vous demande de montrer votre tableau, sans filtre puis avec un filtre ;
  2. tape lui-même une saisie piégée dans l'adresse de votre page ;
  3. vous pose des questions sur votre code ;
  4. vous demande une petite modification, à faire devant lui en 2 minutes ;
  5. regarde votre façon de travailler et d'expliquer.

Il suit cette grille. Lisez-la avant de commencer : c'est exactement ce qui sera noté.

Critère Insuffisant (0 ou 1) Fragile (2) Acquis (3) Maîtrisé (4)
Démonstration · le tableau lit la base La page ne s'affiche pas, ou plante sur un cas normal. La page s'affiche, mais le filtre ou le nom du chantier manque. Toutes les interventions, avec leur chantier, et le filtre par service marche. Tout marche, et le total d'heures correspond aux lignes montrées.
Résistance · la saisie piégée reste sans effet La saisie affiche des données en plus, ou une trace d'erreur. La saisie est bloquée, mais seulement par le menu ou par un test fragile. La requête est paramétrée : la saisie ne change rien. La requête est paramétrée, et vous le prouvez vous-même devant le formateur.
Questions techniques · vous expliquez votre code Vous ne savez pas dire ce que fait votre requête. Vous l'expliquez avec de l'aide. Vous expliquez la requête, le JOIN et le rôle de %s. Vous expliquez aussi, avec vos mots, pourquoi l'attaque échoue.
Travail personnel · la modification en direct Vous ne trouvez pas où modifier. Vous trouvez l'endroit, mais la modification n'aboutit pas. La modification marche en 2 minutes. Elle marche, et vous la testez vous-même sans qu'on vous le demande.
Professionnalisme · votre façon de travailler Réponses floues, dossier en désordre, rien n'est dit de ce qui ne marche pas. Vocabulaire approximatif, explications à reprendre. Mots du métier, explications claires. Vous dites ce qui ne marche pas et l'aide reçue. En plus, vous signalez un risque qui reste, ou une amélioration possible.

La note est la somme des cinq lignes, sur 20.

Ce dont vous disposez

Pour lire le service choisi dans l'adresse (/tableau?service=atelier), la vue utilise request.args.get("service"). Il renvoie None si l'adresse ne contient pas de service, et un texte vide "" si on choisit « Tous les services » dans le menu. Écrivez request.args.get("service") or None : les deux cas donnent alors None.

CE QU'ON VOUS DEMANDE

Le tableau de bord a une seule page, /tableau. Elle affiche toutes les interventions, avec le nom de leur chantier. Un menu filtre les interventions par service.

La fonction interventions_par_service de la couche d'accès est à écrire. La vue qui l'appelle est à compléter. La saisie du service ne doit jamais pouvoir changer la requête envoyée à la base.

Pendant l'entretien, le formateur tapera lui-même un service piégé dans votre page. Votre tableau doit rester sain : aucune donnée qui ne devrait pas s'afficher, aucune trace d'erreur.

PAR OÙ COMMENCER

45 minutes en tout : environ 40 minutes de travail, et l'entretien de 5 minutes dès que votre tableau marche.

  1. Préparez votre poste avec l'annexe  : dossier extrait, MySQL lancé, base chargée, technovert.env rempli. Lancez le tableau de bord et ouvrez /tableau. Environ 5 min.
  2. Écrivez interventions_par_service d'abord sans filtre : la requête relie l'intervention à son chantier avec JOIN, et renvoie tout. Environ 10 min.
  3. Ajoutez le filtre : quand un service est donné, ajoutez WHERE i.service = %s et passez le service en paramètre. Environ 10 min.
  4. Complétez la vue tableau() : elle lit le service avec request.args.get, appelle la couche, et calcule le total des heures. Le gabarit attend aussi message : c'est un texte à afficher quand le service demandé n'est pas dans SERVICES, sinon None. Environ 10 min.
  5. Testez avec un service normal, puis avec une injection de votre choix. Environ 5 min.

CE QUI DOIT MARCHER À LA FIN

Bloqué ? Dites-le au formateur pendant l'entretien. Savoir expliquer où l'on bloque fait partie du professionnalisme.

Annexes

Tous les documents du module

Cliquez sur une annexe pour l'ouvrir dans le panneau de droite. Elle reste ouverte pendant que vous lisez.