Repérer et corriger des requêtes de base de données non sûres

SQLiteBeginner
Pratiquer maintenant

Introduction

Un nom fourni par un utilisateur doit rester une valeur, sans devenir une partie d'une commande de base de données. Cette recherche DeskLite construit actuellement du texte SQL directement à partir d'un nom. Les requêtes ordinaires semblent fonctionner, mais les noms contenant une apostrophe et les entrées spécialement formées révèlent le problème.

Vous lirez seulement une fonction Python de cinq lignes, découvrirez le peu de SQL qu'elle utilise, suivrez une véritable détection Semgrep et appliquerez un modèle de liaison de paramètres SQLite. Aucune connaissance préalable de Python ou de SQL n'est requise. Le programme de lancement, la base de données, le scanner et la vérification fonctionnelle sont déjà préparés.

Exécuter une recherche ordinaire

Dans cette étape, vous établirez ce que la recherche est censée renvoyer avant de lire son implémentation.

DeskLite stocke trois tickets fictifs dans une petite base SQLite. SQLite est un moteur de base de données qui lit et interroge un fichier local. Le cours a déjà préparé la connexion et les données.

cd /home/labex/project/ticket-queries

run_lookup.py est un point d'entrée préparé. Comme dans le lab sur les dépendances, python3 exécute un programme fourni. Le dernier argument est le nom à rechercher :

python3 run_lookup.py Alice

Un ticket avec l'ID 1 et le titre Printer offline est attendu. Le programme de lancement met en forme les lignes de la base de données pour produire la réponse JSON habituelle. Vous modifierez la petite fonction de recherche, pas le code de mise en forme du programme de lancement.

La limite souhaitée est simple : le nom fourni sélectionne les tickets de ce propriétaire. Il ne doit pas modifier le sens de la commande de base de données ni révéler les tickets d'autres propriétaires. Tous les noms et enregistrements de cet exercice sont fictifs ; travaillez uniquement avec cette base locale préparée.

Lire la fonction de cinq lignes

Dans cette étape, vous suivrez le nom fourni à travers une petite fonction Python.

cat lookup.py

Le fichier contient :

from fixture import db

def lookup(name):
    query = f"SELECT id, title FROM tickets WHERE owner = '{name}'"
    return db.execute(query).fetchall()

from fixture import db rend la connexion à la base de données préparée disponible sous le nom db. Une fonction est un ensemble nommé de comportements réutilisables. def lookup(name): définit une fonction dont l'entrée s'appelle name. Les lignes indentées appartiennent à cette fonction ; conservez leur indentation lors des modifications.

Une variable donne un nom à une valeur. query = ... affecte une valeur textuelle, appelée chaîne de caractères, à query. Le f initial en fait une f-string : Python insère la valeur actuelle de name à l'emplacement {name}. Si l'entrée est Alice, ce texte devient une partie de la chaîne SQL.

db.execute(query) est un appel de méthode, semblable à un appel de fonction, qui demande à la connexion d'exécuter la requête. .fetchall() collecte les lignes du résultat. return renvoie ces lignes à l'appelant. Le programme de lancement préparé transmet votre argument de terminal à lookup et affiche les lignes renvoyées.

Vous avez seulement besoin de ce cheminement des données : nom fourni → chaîne de requête → appel à la base de données → lignes renvoyées. Vous lirez ensuite la signification de cette chaîne SQL.

Lire la condition SQL prévue

Dans cette étape, vous relierez la condition SQL aux enregistrements qui doivent être renvoyés.

SQL est un langage permettant de demander des opérations sur une base de données. Cette requête n'en utilise qu'une petite partie :

SELECT id, title FROM tickets WHERE owner = 'Alice'

SELECT id, title choisit les deux champs du résultat. FROM tickets choisit la table préparée. WHERE owner = 'Alice' limite les résultats aux lignes dont le propriétaire est égal à cette valeur textuelle. En SQL, les apostrophes délimitent une valeur textuelle littérale. Le = dans cette chaîne SQL exprime la condition sur le propriétaire. L'affectation Python query = ..., à l'extérieur de la chaîne, donne un nom de variable au texte complet.

Essayez un autre nom ordinaire :

python3 run_lookup.py Bob

Seul le ticket d'ID 3, intitulé Spare keyboard, est attendu. Le nom change la valeur prévue de la condition sur le propriétaire, pas les colonnes sélectionnées ni la table.

Reliez maintenant cela à la f-string que vous avez lue. Python insère le nom fourni entre les apostrophes SQL avant que SQLite ne voie la requête. Cela mélange deux choses différentes : la structure de la commande et les données d'entrée. Une apostrophe dans un véritable nom peut perturber ces limites.

Ne modifiez encore rien. Vous utiliserez d'abord le scanner fixe pour repérer la construction risquée, puis confirmerez le comportement avec un nom légitime contenant une apostrophe et une entrée de test locale autorisée.

Suivre la détection de l'analyse statique

Dans cette étape, vous lirez une véritable détection Semgrep et relierez son emplacement à la construction de la requête.

Les tests statiques de sécurité des applications (SAST) examinent le code sans exécuter toutes les entrées possibles. Le lanceur préparé course-code-scan utilise le moteur Semgrep CE 1.145.0 à version fixée et une règle locale fixe. Aucun compte cloud ni jeu de règles en ligne n'intervient.

course-code-scan lookup.py

Une détection à lookup.py:5 est attendue, avec l'ID de règle desklite.sql-parameter-binding et cette explication :

Bind the supplied lookup name as a SQL parameter instead of building SQL text from it.

La ligne indique l'utilisation de query dans l'appel à la base de données. Remontez cette valeur d'une ligne pour voir où la f-string a été construite. Il s'agit d'une courte investigation du flux de données, pas d'une demande de développement d'une application complète.

Une détection produit le code de sortie 1 ; CHECK ERROR produit le code 2 lorsque le fichier ne peut pas être analysé. Aucun des deux n'est un résultat sans problème. La règle locale suit le name fourni par ce cours jusque dans le texte SQL. Son périmètre est limité : une détection nécessite toujours une revue du code, et l'absence de détection avec une seule règle ne prouve pas qu'une application n'a aucun autre risque.

Vous comparerez ensuite la détection au comportement réel de la recherche.

Comparer un nom avec apostrophe à une entrée qui modifie la requête

Dans cette étape, vous observerez pourquoi une entrée doit rester une donnée, en utilisant uniquement le jeu de données local préparé.

Un propriétaire légitime s'appelle O'Neil. Les guillemets doubles maintiennent l'ensemble de cet argument de terminal, y compris son apostrophe :

python3 run_lookup.py "O'Neil"

Une ligne commençant par Lookup failed: avec une erreur de syntaxe SQLite est attendue. Cet échec constitue une preuve intentionnelle du défaut. L'apostrophe du nom a terminé une valeur textuelle SQL littérale, car le programme l'a insérée directement dans le texte de la commande. Rejeter toutes les apostrophes casserait la recherche d'un véritable propriétaire.

Essayez maintenant cette entrée de test au périmètre limité :

python3 run_lookup.py "' OR 1=1 --"

La version non sûre renvoie les trois tickets au lieu de ne trouver aucun propriétaire correspondant. Dans ce contexte SQL, l'apostrophe ferme la valeur littérale, OR 1=1 rend la condition vraie et -- transforme le texte restant en commentaire. L'entrée a modifié la structure de la commande au lieu de rester un nom. Il s'agit d'une injection SQL.

Vous ne testez pas un service externe et ne modifiez pas les données. La base préparée est en lecture seule et contient des enregistrements fictifs. La correction requise doit satisfaire les deux observations : un nom légitime contenant une apostrophe fonctionne, tandis qu'un texte qui modifie la requête ne peut pas élargir les lignes renvoyées.

Lier le nom séparément du texte SQL

Dans cette étape, vous corrigerez la recherche à l'aide de l'interface de liaison de paramètres de SQLite.

Un marqueur de paramètre indique où doit se trouver une valeur de donnée. Le marqueur ? de SQLite fait partie de la structure SQL fixe ; le nom est fourni séparément à execute. Le pilote de base de données traite la valeur fournie comme une donnée, même si elle contient des apostrophes ou des caractères ressemblant à du SQL.

nano lookup.py

Remplacez les lignes de requête et d'exécution de la fonction par ce modèle pris en charge, en conservant l'import, le nom de la fonction et l'indentation. Enregistrez avec Ctrl+O, Enter, Ctrl+X :

from fixture import db

def lookup(name):
    query = "SELECT id, title FROM tickets WHERE owner = ?"
    return db.execute(query, (name,)).fetchall()

Ne placez pas d'apostrophes SQL autour de ?. Le pilote gère la représentation de la valeur. (name,) est un tuple à un élément, une collection ordonnée de valeurs. La virgule finale en fait un tuple ; son unique valeur correspond à l'unique marqueur. Le deuxième argument d'execute transporte ce tuple séparément du texte de la requête.

Exécutez une recherche ordinaire :

python3 run_lookup.py Alice

Puis réessayez le nom légitime contenant une apostrophe :

python3 run_lookup.py "O'Neil"

Le ticket d'ID 1 est attendu pour Alice, et le ticket d'ID 2, intitulé Login issue, pour O'Neil. Les deux utilisent la même structure SQL fixe. Si Python signale une erreur d'indentation ou de syntaxe, rétablissez la courte fonction ci-dessus ; ne modifiez ni la base de données ni le programme de lancement.

Vérifier le rejet et le comportement légitime

Dans cette étape, vous confirmerez que la correction préserve les noms valides et empêche le texte fourni d'élargir la recherche.

Répétez l'entrée qui modifiait la requête :

python3 run_lookup.py "' OR 1=1 --"

Une liste tickets vide est maintenant attendue. SQLite recherche un propriétaire littéralement nommé par ce texte, et il n'en existe aucun. Aucune structure de requête n'a été ajoutée.

Vérifiez un propriétaire ordinaire absent :

python3 run_lookup.py "Missing owner"

Une autre liste vide est attendue. Une valeur absente est différente d'une application défectueuse : les noms ordinaires et les noms légitimes contenant une apostrophe renvoient toujours leurs propres tickets.

Exécutez la vérification de non-régression fonctionnelle fournie avec le même mode de lancement que dans le lab sur les dépendances :

bash test.sh

Résultat attendu :

PASS: ordinary, quoted, missing, and injection-like names.

Ces vérifications exercent de véritables appels SQLite et lignes de résultat. Supprimer la fonction, renvoyer une liste vide pour toutes les entrées ou supprimer les données ne préserverait pas les résultats légitimes. Une liste noire de caractères est également insuffisante : un véritable propriétaire possède une apostrophe dans son nom. La défense souhaitée est la séparation du texte SQL fixe et des valeurs liées.

Réanalyser le code source corrigé

Dans cette étape, vous combinerez le résultat statique avec le comportement réel de la recherche que vous venez de tester.

course-code-scan lookup.py

Le message attendu est No findings under the fixed local rule. Le nom fourni ne se retrouve plus dans la chaîne de commande SQL. Il est transmis en tant que valeur liée séparée.

Exécutez à nouveau la vérification fonctionnelle :

bash test.sh

Le même résultat de réussite est attendu. L'analyse et le test de comportement répondent à des questions différentes : l'une examine le motif de code ciblé ; l'autre vérifie que les vrais noms fonctionnent toujours et qu'une entrée spéciale ne peut pas élargir le résultat.

La vérification du cours observe aussi les véritables arguments SQLite : simplement masquer une détection du scanner ou remplacer manuellement des apostrophes ne constitue donc pas la correction requise. Conservez la base de données, le jeu de données, le programme de lancement et l'interface de fonction d'origine. Aucun fichier d'investigation écrit n'est nécessaire.

Ce lab a enseigné un petit modèle de liaison SQLite et une règle SAST locale. Une revue complète d'application examinerait aussi d'autres sources d'entrée et catégories de vulnérabilités. Ici, vous avez supprimé le risque précis de passage de l'entrée dans la requête tout en préservant la recherche prévue.

Résumé

Vous avez suivi un nom fourni à travers une courte recherche Python et une condition SQL, relié une véritable détection Semgrep locale au comportement à l'exécution et utilisé la liaison de paramètres SQLite. Les noms ordinaires et ceux contenant une apostrophe fonctionnent toujours, tandis qu'une entrée qui modifie la requête ne peut pas élargir les résultats. Les preuves statiques et fonctionnelles étayent la limite précise qui a été corrigée.