Choisis un module pour tester tes connaissances. Tout tourne en local dans ce fichier — aucune donnée n'est envoyée nulle part.
MODULE 01
Bac à sable SQL
Une vraie base SQLite en mémoire (clients, commandes, produits) — écris et exécute tes propres requêtes.
> lancer_module --sandbox
MODULE 02
Cours & Aide-mémoire
Chaque commande SQL et Linux expliquée en une phrase, avec un exemple concret. À garder ouvert pendant que tu pratiques.
> lancer_module --reference
MODULE 03
Cybersécurité SQL LAB LOCAL
Comprends l'injection SQL de l'intérieur : login, UNION, ORDER BY, requêtes empilées, LIKE, second ordre, blind (booléenne & temporelle) — puis la remédiation, avec un mode objectif façon CTF.
> lancer_module --cybersec
MODULE 04
Quiz SQL
20 questions niveau débutant/intermédiaire — SELECT, JOIN, GROUP BY, sous-requêtes, fenêtrage...
Ce labo simule localement une application vulnérable (base de démo, mots de passe factices) pour comprendre comment et pourquoi l'injection SQL fonctionne — dans le seul but de mieux s'en défendre. Rien ici ne touche un vrai système.
La requête, construite par simple concaténation de chaînes, ressemble à ceci côté serveur :
SELECT * FROM users WHERE username = '...' AND password = '...'
Ce que ça donne, une fois la chaîne injectée dans la requête réelle (essaie les payloads ci-dessus puis regarde la requête générée) :
Ici, une fonctionnalité « rechercher un client par ID » construit sa requête ainsi (paramètre numérique, sans guillemets) :
SELECT id, username AS info FROM users WHERE id = <ton_entrée>
La 1ère charge affiche tous les comptes (contournement du filtre). La 2ᵉ détourne la colonne « info », censée montrer un pseudo, pour y faire apparaître des mots de passe — c'est le principe de l'extraction par UNION : faire correspondre le nombre et le type de colonnes à la requête d'origine.
Un tableau de produits triable construit sa requête comme ceci — le paramètre de tri n'est jamais une simple valeur, donc rarement filtré comme le reste :
SELECT id, nom_produit, prix FROM produits ORDER BY <ton_entrée>
La 1ère charge est inoffensive. La 2ᵉ transforme le tri en test booléen caché : selon qu'une condition est vraie ou fausse, l'ordre des lignes change — une variante de blind SQLi qui n'a même pas besoin d'un message d'erreur. La 3ᵉ provoque une erreur SQL révélatrice : le simple fait qu'elle apparaisse prouve que l'entrée n'est pas filtrée.
Certains services exécutent directement une chaîne construite par l'application, sans API limitant à une seule instruction. Le caractère ; permet alors d'enchaîner des commandes totalement différentes de la requête prévue :
La base de démo est automatiquement restaurée après ce test pour que tu puisses continuer à t'entraîner. Dans la vraie vie, la plupart des connecteurs modernes (mysqli, psycopg2, PDO en usage standard) bloquent nativement l'exécution multi-instructions — mais ce n'est pas garanti partout, d'où l'importance de ne jamais compter dessus et d'utiliser des requêtes préparées.
Cette fois, l'appli ne renvoie plus jamais de données : juste « Accès autorisé » ou « Accès refusé ». C'est le scénario classique du blind SQLi : on ne voit rien, mais une réponse booléenne suffit à deviner les données caractère par caractère.
SELECT * FROM users WHERE username = 'admin' AND password = '<ton_payload>'
requêtes envoyées : 0
Astuce : en changeant la lettre testée par SUBSTR(password,1,1), puis la position (,2,1, ,3,1...), tu peux reconstituer le mot de passe de « admin » lettre par lettre — sans jamais le voir directement affiché.
Un moteur de recherche d'avis clients construit sa requête en insérant le mot-clé tapé directement entre deux % :
SELECT id, commentaire FROM avis WHERE commentaire LIKE '%<ton_entrée>%'
Le % n'est qu'un caractère joker parmi d'autres dans le motif LIKE : dès qu'on referme les guillemets simples (') avant lui, on retrouve une injection classique — la 1ère charge affiche tous les avis, la 2ᵉ détourne les deux colonnes de la recherche pour faire fuiter les identifiants de la table users, exactement comme avec UNION.
Ici, l'injection ne se déclenche pas au moment où tu envoies la donnée : ton pseudo est stocké tel quel (requête préparée à l'inscription, donc rien ne se passe tout de suite). Le problème surgit plus tard, quand un traitement interne (ex : un rapport journalier) réutilise ce pseudo stocké dans une requête construite par concaténation.
Quelques temps après, le job interne relit ce pseudo tel quel pour retrouver la fiche du client, cette fois par simple concaténation :
SELECT * FROM clients WHERE nom = '<pseudo_stocké>'
Ce décalage dans le temps (et souvent dans le module) rend la seconde-ordre plus difficile à repérer qu'une injection classique : la donnée a l'air propre partout où on la valide, sauf là où, bien plus tard, quelqu'un a fait confiance à une donnée « déjà en base » et l'a reconcaténée sans y penser.
Cette fois, l'appli ne renvoie ni donnée, ni différence visible (même écran de chargement, quel que soit le résultat). Seul indice exploitable : le temps de réponse. En conditionnant un calcul volontairement lourd à une expression booléenne, une condition vraie met sensiblement plus de temps à répondre qu'une condition fausse.
SELECT * FROM users WHERE username = 'admin' AND password = '<ton_payload>'
requêtes envoyées : 0
Compare le temps mesuré à la charge de référence (« x », sans condition) : un écart net signale que la condition CASE WHEN était vraie, sans qu'aucune donnée n'ait jamais été affichée à l'écran. En pratique (MySQL/PostgreSQL), on utiliserait SLEEP() ou pg_sleep() ; SQLite n'a pas de fonction de pause native, donc ce labo simule le même principe avec un calcul récursif volontairement coûteux.
Mode objectif façon CTF : chaque défi te donne un but précis à atteindre (pas juste « injecte quelque chose ») — à toi de trouver le bon payload. Indice et solution disponibles si tu bloques.
OBJECTIF
REMEDIATION — comment s'en protéger
Le problème n'est jamais SQL en lui-même, mais la concaténation de texte non fiable dans une requête. Les parades standard :
— Requêtes préparées / paramétrées : SELECT * FROM users WHERE username = ? AND password = ? avec les valeurs liées séparément — l'entrée n'est jamais interprétée comme du SQL.
— Validation et typage stricts des entrées (un ID attendu en entier ne doit accepter que des chiffres).
— Principe du moindre privilège : le compte SQL de l'application ne doit pas pouvoir lire des tables hors de son besoin.
— Ne jamais stocker de mots de passe en clair (hash + sel, ex. bcrypt/argon2) — ici ils sont en clair uniquement à des fins pédagogiques.
Chaque commande, en une phrase, avec un exemple. Pas de blabla — juste de quoi comprendre vite et retrouver un réflexe pendant que tu pratiques.
FICHIER LOCAL — AUCUNE CONNEXION SAUVEGARDEE — RAFRAICHIR REINITIALISE LES SCORES