Prévenir les attaques par injection SQL avec Python – Real Python

By | septembre 30, 2019

trouver un expert Python

Toutes les quelques années, le projet OWASP (Open Web Application Security) classe les risques de sécurité des applications Web les plus critiques. Depuis le premier rapport, les risques d'injection ont toujours été au top. Parmi tous les types d'injection, Injection SQL est l'un des vecteurs d'attaque les plus courants, et sans doute le plus dangereux. Python étant l’un des langages de programmation les plus populaires au monde, il est essentiel de savoir comment se protéger contre l’injection Python SQL.

Dans ce tutoriel, vous allez apprendre:

  • Quoi Injection SQL Python est et comment le prévenir
  • Comment composer des requêtes avec des littéraux et des identifiants comme paramètres
  • Comment exécuter des requêtes en toute sécurité dans une base de données

Ce tutoriel est adapté pour utilisateurs de tous les moteurs de base de données. Les exemples ici utilisent PostgreSQL, mais les résultats peuvent être reproduits dans d’autres systèmes de gestion de bases de données (tels que SQLite, MySQL, Microsoft SQL Server, Oracle, etc.).

Comprendre l'injection SQL Python

Les attaques par injection SQL sont une telle vulnérabilité de sécurité que le légendaire xkcd webcomic y a consacré une bande dessinée:

Un webcomic humoristique de xkcd sur l’effet potentiel de l’injection SQL
"Exploits of a Mom" ​​(Image: xkcd)

Générer et exécuter des requêtes SQL est une tâche courante. Cependant, les entreprises du monde entier commettent souvent d’horribles erreurs lors de la composition des instructions SQL. Alors que la couche ORM compose généralement des requêtes SQL, vous devez parfois écrire les vôtres.

Lorsque vous utilisez Python pour exécuter ces requêtes directement dans une base de données, il est possible que vous commettiez des erreurs pouvant compromettre votre système. Dans ce didacticiel, vous apprendrez à implémenter avec succès des fonctions qui composent des requêtes SQL dynamiques. sans pour autant exposer votre système à des risques d’injection Python SQL.

Mise en place d'une base de données

Pour commencer, vous allez configurer une nouvelle base de données PostgreSQL et l’alimenter avec des données. Tout au long du didacticiel, vous utiliserez cette base de données pour constater par vous-même comment fonctionne l’injection Python SQL.

Création d'une base de données

Tout d'abord, ouvrez votre shell et créez une nouvelle base de données PostgreSQL appartenant à l'utilisateur postgres:

$ createdb -O postgres psycopgtest

Ici vous avez utilisé l'option de ligne de commande -O définir le propriétaire de la base de données sur l'utilisateur postgres. Vous avez également spécifié le nom de la base de données, qui est psycopgtest.

Votre nouvelle base de données est prête à partir! Vous pouvez vous y connecter en utilisant psql:

$ psql -U postgres -d psycopgtest
psql (11.2, serveur 10.5)
Tapez "aide" pour obtenir de l'aide.

Vous êtes maintenant connecté à la base de données psycopgtest en tant qu'utilisateur postgres. Cet utilisateur est également le propriétaire de la base de données. Vous disposez donc d'autorisations de lecture sur toutes les tables de la base de données.

Créer une table avec des données

Ensuite, vous devez créer une table avec des informations utilisateur et y ajouter des données:

psycopgtest = # CRÉER TABLE utilisateurs (
    Nom d'utilisateur Varchar(30),
    admin booléen
)
CREER LA TABLE

psycopgtest = # INSÉRER DANS utilisateurs
    (Nom d'utilisateur, admin)
VALEURS
    ('a couru', vrai),
    ('haki', faux)
INSÉRER 0 2

psycopgtest = # SÉLECTIONNER * DE utilisateurs;
    nom d'utilisateur | admin
---------- + -------
    a couru | t
    haki | F
(2 rangs)

Le tableau a deux colonnes: Nom d'utilisateur et admin. le admin La colonne indique si un utilisateur a ou non des privilèges d'administrateur. Votre objectif est de cibler le admin terrain et essayer d’en abuser.

Configuration d'un environnement virtuel Python

Maintenant que vous avez une base de données, il est temps de configurer votre environnement Python. Pour obtenir des instructions détaillées sur la procédure à suivre, consultez le document intitulé Environnements virtuels Python: un guide d’introduction.

Créez votre environnement virtuel dans un nouveau répertoire:

~ / src $ mkdir psycopgtest
~ / src $ cd psycopgtest
~ / src / psycopgtest $ python3 -m venv venv

Après avoir exécuté cette commande, un nouveau répertoire appelé venv sera créé. Ce répertoire stockera tous les packages que vous installez dans l'environnement virtuel.

Connexion à la base de données

Pour vous connecter à une base de données en Python, vous avez besoin d’un adaptateur de base de données. La plupart des adaptateurs de base de données suivent la version 2.0 de la spécification PEP 249 de l'API de base de données Python. Chaque moteur de base de données majeur dispose d'un adaptateur de premier plan:

Pour vous connecter à une base de données PostgreSQL, vous devez installer Psycopg, l’adaptateur le plus populaire pour PostgreSQL en Python. Django ORM l’utilise par défaut et est également pris en charge par SQLAlchemy.

Dans votre terminal, activez l'environnement virtuel et utilisez pépin à installer psycopg:

~ / src / psycopgtest $ source venv / bin / activate
~ / src / psycopgtest $ python -m installation de pip psycopg2> = 2.8.0
Collecte de psycopg2
        Utilisation de https mis en cache: // ....
        psycopg2-2.8.2.tar.gz
Installation des packages collectés: psycopg2
        Lancer setup.py install pour psycopg2 ... done
Psycopg2-2.8.2 installé avec succès

Vous êtes maintenant prêt à créer une connexion à votre base de données. Voici le début de votre script Python:

importation psycopg2

lien = psycopg2.relier(
    hôte="localhost",
    base de données="psycopgtest",
    utilisateur="postgres",
    mot de passe=Aucun,
)
lien.set_session(autocommit=Vrai)

Vous avez utilisé psycopg2.connect () pour créer la connexion. Cette fonction accepte les arguments suivants:

  • hôte est l'adresse IP ou le DNS du serveur sur lequel se trouve votre base de données. Dans ce cas, l’hôte est votre ordinateur local ou localhost.

  • base de données est le nom de la base de données à laquelle se connecter. Vous souhaitez vous connecter à la base de données créée précédemment, psycopgtest.

  • utilisateur est un utilisateur avec des autorisations pour la base de données. Dans ce cas, vous souhaitez vous connecter à la base de données en tant que propriétaire, afin de transmettre l'utilisateur postgres.

  • mot de passe est le mot de passe pour celui que vous avez spécifié dans utilisateur. Dans la plupart des environnements de développement, les utilisateurs peuvent se connecter à la base de données locale sans mot de passe.

Après avoir configuré la connexion, vous avez configuré la session avec autocommit = True. L'activation autocommit signifie que vous n'aurez pas à gérer manuellement les transactions en émettant un commettre ou retour en arriere. C'est la valeur par défaut
comportement
dans la plupart des ORM. Vous utilisez également ce comportement ici pour pouvoir vous concentrer sur la composition de requêtes SQL au lieu de gérer des transactions.

Exécuter une requête

Maintenant que vous êtes connecté à la base de données, vous êtes prêt à exécuter une requête:

>>>

>>> avec lien.le curseur() comme le curseur:
...     le curseur.exécuter('SELECT COUNT (*) FROM users')
...     résultat = le curseur.fetchone()
... impression(résultat)
(2)

Vous avez utilisé le lien objet pour créer un le curseur. Tout comme un fichier en Python, le curseur est implémenté en tant que gestionnaire de contexte. Lorsque vous créez le contexte, un le curseur est ouvert pour que vous puissiez envoyer des commandes à la base de données. Lorsque le contexte se termine, le le curseur ferme et vous ne pouvez plus l'utiliser.

Dans le contexte, vous avez utilisé le curseur pour exécuter une requête et récupérer les résultats. Dans ce cas, vous avez émis une requête pour compter les lignes de la liste. utilisateurs table. Pour récupérer le résultat de la requête, vous avez exécuté curseur.fetchone () et a reçu un tuple. Comme la requête ne peut renvoyer qu'un seul résultat, vous avez utilisé fetchone (). Si la requête devait renvoyer plusieurs résultats, vous devrez soit effectuer une itération sur le curseur ou utiliser l'un des autres chercher * méthodes.

Utilisation de paramètres de requête dans SQL

Dans la section précédente, vous avez créé une base de données, établi une connexion à cette base et exécuté une requête. La requête que vous avez utilisée était statique. En d'autres termes, il avait pas de paramètres. Vous allez maintenant commencer à utiliser des paramètres dans vos requêtes.

Tout d’abord, vous allez implémenter une fonction qui vérifie si un utilisateur est ou non un administrateur. is_admin () accepte un nom d'utilisateur et renvoie le statut administrateur de cet utilisateur:

# Mauvais exemple. NE FAITES PAS CELA!
def is_admin(Nom d'utilisateur: str) -> bool:
    avec lien.le curseur() comme le curseur:
        le curseur.exécuter("" "
                                                SÉLECTIONNER
                                                                admin
                                                DE
                                                                utilisateurs

                                                                nom d'utilisateur = '% s'
                                "" " % Nom d'utilisateur)
        résultat = le curseur.fetchone()
    admin, = résultat
    revenir admin

Cette fonction exécute une requête pour extraire la valeur du admin colonne pour un nom d'utilisateur donné. Vous avez utilisé fetchone () renvoyer un tuple avec un seul résultat. Ensuite, vous avez décompressé ce tuple dans la variable admin. Pour tester votre fonction, vérifiez quelques noms d'utilisateurs:

>>>

>>> is_admin('haki')
Faux
>>> is_admin('a couru')
Vrai

Jusqu'ici tout va bien. La fonction a renvoyé le résultat attendu pour les deux utilisateurs. Mais qu'en est-il des utilisateurs non existants? Jetez un oeil à cette traceback Python:

>>>

>>> is_admin('foo')
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 12, dans is_admin
Erreur-type: ne peut pas décompresser un objet None-Type non-itérable

Lorsque l'utilisateur n'existe pas, un Erreur-type est élevé. Ceci est dû au fait .fetchone () résultats Aucun quand aucun résultat n'est trouvé, et décompresser Aucun soulève un Erreur-type. Le seul endroit où vous pouvez décompresser un tuple est l'endroit où vous peuplez admin de résultat.

Pour gérer des utilisateurs non existants, créez un cas spécial pour résultat est Aucun:

# Mauvais exemple. NE FAITES PAS CELA!
def is_admin(Nom d'utilisateur: str) -> bool:
    avec lien.le curseur() comme le curseur:
        le curseur.exécuter("" "
                                                SÉLECTIONNER
                                                                admin
                                                DE
                                                                utilisateurs

                                                                nom d'utilisateur = '% s'
                                "" " % Nom d'utilisateur)
        résultat = le curseur.fetchone()

    si résultat est Aucun:
        # L'utilisateur n'existe pas
        revenir Faux

    admin, = résultat
    revenir admin

Ici, vous avez ajouté un cas spécial pour la manipulation Aucun. Si Nom d'utilisateur n'existe pas, alors la fonction devrait retourner Faux. Encore une fois, testez la fonction sur certains utilisateurs:

>>>

>>> is_admin('haki')
Faux
>>> is_admin('a couru')
Vrai
>>> is_admin('foo')
Faux

Génial! La fonction peut désormais gérer des noms d'utilisateurs non existants.

Exploitation des paramètres de requête avec l'injection SQL Python

Dans l'exemple précédent, vous utilisiez une interpolation de chaîne pour générer une requête. Ensuite, vous avez exécuté la requête et envoyé la chaîne résultante directement à la base de données. Cependant, vous avez peut-être oublié quelque chose au cours de ce processus.

Repensez à la Nom d'utilisateur argument que vous avez passé à is_admin (). Que représente exactement cette variable? Vous pourriez supposer que Nom d'utilisateur est juste une chaîne qui représente le nom d’un utilisateur réel. Comme vous êtes sur le point de le voir, un intrus peut facilement exploiter ce type d’oubli et causer des dommages importants en effectuant une injection Python SQL.

Essayez de vérifier si l'utilisateur suivant est un administrateur ou non:

>>>

>>> is_admin("'; sélectionnez true; -")
Vrai

Attends… Qu'est-ce qui vient de se passer?

Jetons un autre regard sur la mise en œuvre. Imprimez la requête en cours d'exécution dans la base de données:

>>>

>>> impression("sélectionnez admin des utilisateurs où username = '% s'" % "'; sélectionnez true; -")
sélectionnez admin parmi les utilisateurs où username = ''; sélectionnez true; - '

Le texte résultant contient trois déclarations. Pour comprendre exactement le fonctionnement de l'injection SQL Python, vous devez inspecter chaque composant individuellement. La première déclaration est la suivante:

sélectionner admin de utilisateurs  Nom d'utilisateur = '';

Ceci est votre requête prévue. Le point-virgule (;) met fin à la requête, le résultat de cette requête n’importe donc pas. La deuxième déclaration est la suivante:

Cette déclaration a été construite par l'intrus. Il est conçu pour toujours revenir Vrai.

Enfin, vous voyez ce petit morceau de code:

Cet extrait désamorce tout ce qui vient après. L'intrus a ajouté le symbole de commentaire (-) pour transformer tout ce que vous auriez pu mettre après le dernier espace réservé en commentaire.

Lorsque vous exécutez la fonction avec cet argument, il reviendra toujours Vrai. Si, par exemple, vous utilisez cette fonction dans votre page de connexion, un intrus peut se connecter avec le nom d'utilisateur '; sélectionnez true; -et l’accès leur sera accordé.

Si vous pensez que c'est mauvais, cela pourrait empirer! Les intrus connaissant la structure de votre table peuvent utiliser l’injection Python SQL pour causer des dommages permanents. Par exemple, l'intrus peut injecter une instruction de mise à jour pour modifier les informations de la base de données:

>>>

>>> is_admin('haki')
Faux
>>> is_admin("'; mettre à jour les utilisateurs set admin =' true 'où username =' haki '; sélectionnez true; -")
Vrai
>>> is_admin('haki')
Vrai

Répondons à nouveau:

Cet extrait termine la requête, exactement comme lors de l'injection précédente. La déclaration suivante est la suivante:

mise à jour utilisateurs ensemble admin = 'vrai'  Nom d'utilisateur = 'haki';

Cette section met à jour admin à vrai pour l'utilisateur haki.

Enfin, il y a cet extrait de code:

Comme dans l'exemple précédent, cette pièce retourne vrai et commente tout ce qui suit.

Pourquoi est-ce pire? Eh bien, si l'intrus parvient à exécuter la fonction avec cette entrée, alors l'utilisateur haki deviendra un administrateur:

psycopgtest = # sélectionner * de utilisateurs;
    nom d'utilisateur | admin
---------- + -------
    a couru | t
    haki | t
(2 rangs)

L'intrus n'a plus à utiliser le hack. Ils peuvent simplement se connecter avec le nom d'utilisateur haki. (Si l'intrus vraiment voulu causer un préjudice, alors ils pourraient même émettre un DROP DATABASE commander.)

Avant d'oublier, restaurez haki retour à son état d'origine:

psycopgtest = # mise à jour utilisateurs ensemble admin = faux  Nom d'utilisateur = 'haki';
MISE À JOUR 1

Pourquoi cela se produit-il donc? Eh bien, que savez-vous de la Nom d'utilisateur argument? Vous savez qu'il devrait s'agir d'une chaîne représentant le nom d'utilisateur, mais vous ne vérifiez ni n'imposez pas cette assertion. Cela peut être dangereux! C’est exactement ce que recherchent les attaquants qui tentent de pirater votre système.

Crafting Safe Query Parameters

Dans la section précédente, vous avez vu comment un intrus peut exploiter votre système et obtenir des autorisations d'administrateur à l'aide d'une chaîne soigneusement conçue. Le problème était que vous avez autorisé la valeur transmise par le client à être exécutée directement dans la base de données, sans effectuer aucune sorte de vérification ou de validation. Les injections SQL reposent sur ce type de vulnérabilité.

Chaque fois que l’utilisateur entre dans une requête de base de données, il existe une vulnérabilité possible pour l’injection SQL. La clé pour empêcher l’injection de Python SQL est de s’assurer que la valeur est utilisée comme prévu par le développeur. Dans l'exemple précédent, vous aviez l'intention de Nom d'utilisateur être utilisé comme une chaîne. En réalité, il a été utilisé comme une instruction SQL brute.

Pour vous assurer que les valeurs sont utilisées comme prévu, vous devez échapper la valeur. Par exemple, pour empêcher les intrus d’injecter du SQL brut à la place d’un argument de chaîne, vous pouvez échapper les guillemets:

>>>

>>> # Mauvais exemple. NE FAITES PAS CELA!
>>> Nom d'utilisateur = Nom d'utilisateur.remplacer("'", "''")

Ceci n'est qu'un exemple. Il y a beaucoup de personnages spéciaux et de scénarios à prendre en compte lorsque l'on tente d'empêcher l'injection de Python SQL. Heureusement pour vous, les adaptateurs de base de données modernes, sont livrés avec des outils intégrés pour empêcher l’injection de Python SQL en utilisant paramètres de requête. Celles-ci sont utilisées à la place de l’interpolation de chaîne simple pour composer une requête avec des paramètres.

Maintenant que vous comprenez mieux la vulnérabilité, vous êtes prêt à réécrire la fonction en utilisant des paramètres de requête au lieu de l’interpolation de chaîne:

    1 def is_admin(Nom d'utilisateur: str) -> bool:
    2     avec lien.le curseur() comme le curseur:
    3         le curseur.exécuter("" "
    4                                                 SÉLECTIONNER
    5                                                                 admin
    6                                                 DE
    sept                                                                 utilisateurs
    8 
    9                                                                 nom d'utilisateur = % (nom d'utilisateur) s
dix                                 "" ", 
11             'Nom d'utilisateur': Nom d'utilisateur
12         )
13         résultat = le curseur.fetchone()
14 
15     si résultat est Aucun:
16         # L'utilisateur n'existe pas
17         revenir Faux
18 
19     admin, = résultat
20     revenir admin

Voici ce qui est différent dans cet exemple:

  • À la ligne 9, vous avez utilisé un paramètre nommé Nom d'utilisateur pour indiquer où le nom d'utilisateur doit aller. Remarquez comment le paramètre Nom d'utilisateur n'est plus entouré de guillemets simples.

  • À la ligne 11, vous avez passé la valeur de Nom d'utilisateur comme deuxième argument à cursor.execute (). La connexion utilisera le type et la valeur de Nom d'utilisateur lors de l'exécution de la requête dans la base de données.

Pour tester cette fonction, essayez des valeurs valides et non valides, y compris la chaîne dangereuse d’avant:

>>>

>>> is_admin('haki')
Faux
>>> is_admin('a couru')
Vrai
>>> is_admin('foo')
Faux
>>> is_admin("'; sélectionnez true; -")
Faux

Incroyable! La fonction a renvoyé le résultat attendu pour toutes les valeurs. De plus, la chaîne dangereuse ne fonctionne plus. Pour comprendre pourquoi, vous pouvez inspecter la requête générée par exécuter():

>>>

>>> avec lien.le curseur() comme le curseur:
...    le curseur.exécuter("" "
...                             SÉLECTIONNER
...                                             admin
...                             DE
...                                             utilisateurs
... 
...                                             nom d'utilisateur = % (nom d'utilisateur) s
...             "" ", 
...        'Nom d'utilisateur': "'; sélectionnez true; -"
...    )
...    impression(le curseur.requete.décoder('utf-8'))
SÉLECTIONNER
                admin
DE
                utilisateurs

                nom d'utilisateur = '' '; sélectionnez true; - '

La connexion a traité la valeur de Nom d'utilisateur en tant que chaîne et échappé à tous les caractères susceptibles de terminer la chaîne et d'introduire l'injection SQL Python.

Passage de paramètres de requête sécurisés

Les adaptateurs de base de données offrent généralement plusieurs façons de transmettre des paramètres de requête. Espaces réservés nommés sont généralement les meilleurs pour la lisibilité, mais certaines implémentations pourraient tirer avantage d’utiliser d’autres options.

Jetons un coup d’œil sur les bonnes et les mauvaises façons d’utiliser les paramètres de requête. Le bloc de code suivant indique les types de requêtes que vous souhaitez éviter:

# Mauvais exemples. NE FAITES PAS CELA!
le curseur.exécuter("SELECT admin FROM utilisateurs WHERE nom_utilisateur = '" + Nom d'utilisateur + '");
le curseur.exécuter("SELECT admin FROM utilisateurs WHERE nom_utilisateur = '% s' % unom d'utilisateur);
le curseur.exécuter("SELECT admin FROM utilisateurs WHERE nom_utilisateur = ''".format(Nom d'utilisateur));
le curseur.exécuter(F"SELECT admin FROM utilisateurs WHERE nom_utilisateur = 'Nom d'utilisateur'")

Chacune de ces déclarations passe Nom d'utilisateur directement du client vers la base de données, sans effectuer aucune sorte de vérification ou de validation. Ce type de code est mûr pour l'invitation de l'injection SQL Python.

En revanche, vous devez pouvoir exécuter ces types de requêtes en toute sécurité:

# EXEMPLES SAFE. FAIRE CELA!
le curseur.exécuter("SELECT admin FROM utilisateurs WHERE nom_utilisateur = % s'", (Nom d'utilisateur, ));
le curseur.exécuter("SELECT admin FROM utilisateurs WHERE nom_utilisateur = % (nom d'utilisateur) s", 'Nom d'utilisateur': Nom d'utilisateur);

Dans ces déclarations, Nom d'utilisateur est passé en tant que paramètre nommé. Maintenant, la base de données utilisera le type et la valeur spécifiés de Nom d'utilisateur lors de l'exécution de la requête, offre une protection contre l'injection Python SQL.

Utilisation de la composition SQL

Jusqu'à présent, vous avez utilisé des paramètres pour les littéraux. Littéraux sont des valeurs telles que des nombres, des chaînes et des dates. Mais que se passe-t-il si vous avez un cas d'utilisation qui nécessite de composer une requête différente, celle où le paramètre est autre chose, comme un nom de table ou de colonne?

Inspirés de l’exemple précédent, implémentons une fonction qui accepte le nom d’une table et renvoie le nombre de lignes de cette table:

# Mauvais exemple. NE FAITES PAS CELA!
def count_rows(nom de la table: str) -> int:
    avec lien.le curseur() comme le curseur:
        le curseur.exécuter("" "
                                                SÉLECTIONNER
                                                                compter(*)
                                                DE
                % (nom_table) s
                                "" ", 
            'nom de la table': nom de la table,
        )
        résultat = le curseur.fetchone()

    nombre de lignes, = résultat
    revenir nombre de lignes

Essayez d'exécuter la fonction sur votre table d'utilisateurs:

>>>

Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 9, dans count_rows
psycopg2.errors.SyntaxError: erreur de syntaxe à ou près de "'utilisateurs'"
LIGNE 5: 'utilisateurs'
                                                                                                ^

La commande n'a pas pu générer le SQL. Comme vous l'avez déjà vu, l'adaptateur de base de données traite la variable comme une chaîne ou un littéral. Un nom de table, cependant, n'est pas une simple chaîne. C'est ici qu'intervient la composition SQL.

Vous savez déjà qu’il est dangereux d’utiliser l’interpolation de chaînes pour composer du SQL. Heureusement, Psycopg fournit un module appelé psycopg.sql pour vous aider à composer des requêtes SQL en toute sécurité. Réécrivons la fonction en utilisant psycopg.sql.SQL ():

de psycopg2 importation sql

def count_rows(nom de la table: str) -> int:
    avec lien.le curseur() comme le curseur:
        stmt = sql.SQL("" "
                                                SÉLECTIONNER
                                                                compter(*)
                                                DE
                nom de la table
                                "" ").format(
            nom de la table = sql.Identifiant(nom de la table),
        )
        le curseur.exécuter(stmt)
        résultat = le curseur.fetchone()

    nombre de lignes, = résultat
    revenir nombre de lignes

Il y a deux différences dans cette implémentation. Tout d'abord, vous avez utilisé sql.SQL () pour composer la requête. Ensuite, vous avez utilisé sql.Identifier () annoter la valeur de l'argument nom de la table. (Un identifiant est un nom de colonne ou de table.)

Maintenant, essayez d’exécuter la fonction sur le utilisateurs table:

>>>

>>> count_rows('utilisateurs')
2

Génial! Voyons ensuite ce qui se passe lorsque la table n’existe pas:

>>>

>>> count_rows('foo')
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 11, dans count_rows
psycopg2.errors.UndefinedTable: la relation "foo" n'existe pas
LIGNE 5: "foo"
                                                                                                ^

La fonction jette le UndefinedTable exception. Dans les étapes suivantes, vous utiliserez cette exception pour indiquer que votre fonction est protégée contre une attaque par injection Python SQL.

Pour tout mettre ensemble, ajoutez une option permettant de compter les lignes de la table jusqu'à une certaine limite. Cette fonctionnalité peut être utile pour les très grandes tables. Pour implémenter cela, ajoutez un LIMITE à la requête, ainsi que les paramètres de requête pour la valeur de la limite:

de psycopg2 importation sql

def count_rows(nom de la table: str, limite: int) -> int:
    avec lien.le curseur() comme le curseur:
        stmt = sql.SQL("" "
                                                SÉLECTIONNER
                                                                COMPTER(*)
                                                DE (
                                                                SÉLECTIONNER
                                                                                1
                                                                DE
                    nom de la table
                                                                LIMITE
                    limite
                                                ) AS limit_query
                                "" ").format(
            nom de la table = sql.Identifiant(nom de la table),
            limite = sql.Littéral(limite),
        )
        le curseur.exécuter(stmt)
        résultat = le curseur.fetchone()

    nombre de lignes, = résultat
    revenir nombre de lignes

Dans ce bloc de code, vous avez annoté limite en utilisant sql.Literal (). Comme dans l'exemple précédent, psycopg reliera tous les paramètres de la requête en tant que littéraux lors de l'utilisation de l'approche simple. Cependant, lors de l'utilisation sql.SQL (), vous devez annoter explicitement chaque paramètre en utilisant sql.Identifier () ou sql.Literal ().

Exécutez la fonction pour vous assurer que cela fonctionne:

>>>

>>> count_rows('utilisateurs', 1)
1
>>> count_rows('utilisateurs', dix)
2

Maintenant que la fonction fonctionne, assurez-vous qu’elle est également sûre:

>>>

>>> count_rows("(sélectionnez 1) en tant que foo; utilisateurs de mise à jour set admin = true où name = 'haki'; -", 1)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 18, dans count_rows
psycopg2.errors.UndefinedTable: relation "(sélectionnez 1) comme foo; utilisateurs de mise à jour set admin = true où name = '" n'existe pas
LIGNE 8: "(sélectionnez 1) comme foo; les utilisateurs de mise à jour ...
                                                                                                                ^

Cette trace montre que psycopg échappé à la valeur, et la base de données l'a traitée comme un nom de table. Puisqu’une table portant ce nom n’existe pas, une UndefinedTable exception a été soulevée et vous n'avez pas été piraté!

Conclusion

Vous avez implémenté avec succès une fonction qui compose le SQL dynamique sans pour autant exposer votre système à des risques d’injection Python SQL! Vous avez utilisé des littéraux et des identifiants dans votre requête sans compromettre la sécurité.

Vous avez appris:

  • Quoi Injection SQL Python est et comment il peut être exploité
  • Comment empêcher l'injection de Python SQL en utilisant des paramètres de requête
  • Comment composer en toute sécurité des instructions SQL qui utilisent des littéraux et des identifiants comme paramètres

Vous pouvez maintenant créer des programmes capables de résister aux attaques de l’extérieur. Allez de l'avant et contrecarrez les pirates!