Toutes les quelques années, l'Open Web Application Security Project (OWASP) 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, l'injection SQL est l'un des vecteurs d'attaque les plus courants, et sans doute le plus dangereux. Comme Python est 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 didacticiel, vous allez apprendre :
- Qu'est-ce que l'injection Python SQL ? est et comment le prévenir
- Comment composer des requêtes avec à la fois 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 didacticiel s'adresse aux 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 base de données (tels que SQLite, MySQL, Microsoft SQL Server, Oracle, etc.).
Bonus gratuit : 5 Thoughts On Python Mastery, un cours gratuit pour les développeurs Python qui vous montre la feuille de route et l'état d'esprit dont vous aurez besoin pour faire passer vos compétences Python au niveau supérieur.
Comprendre l'injection SQL Python
Les attaques par injection SQL sont une vulnérabilité de sécurité si courante que le légendaire xkcd webcomic lui a consacré une BD :
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 lorsqu'il s'agit de composer 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, vous risquez de commettre des erreurs susceptibles de compromettre votre système. Dans ce didacticiel, vous apprendrez à implémenter avec succès des fonctions qui composent des requêtes SQL dynamiques sans mettre votre système en danger pour l'injection Python SQL.
Configurer une base de données
Pour commencer, vous allez configurer une nouvelle base de données PostgreSQL et la remplir avec des données. Tout au long du didacticiel, vous utiliserez cette base de données pour constater par vous-même le fonctionnement de l'injection SQL Python.
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
pour 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
.
Remarque : postgres
est un utilisateur spécial , que vous réserveriez normalement aux tâches administratives, mais pour ce tutoriel, il est possible d'utiliser postgres
. Dans un système réel, cependant, vous devez créer un utilisateur distinct pour être le propriétaire de la base de données.
Votre nouvelle base de données est prête à fonctionner ! Vous pouvez vous y connecter en utilisant psql
:
$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.
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 aurez donc des autorisations de lecture sur chaque table de la base de données.
Créer un tableau avec des données
Ensuite, vous devez créer un tableau avec des informations sur l'utilisateur et y ajouter des données :
psycopgtest=# CREATE TABLE users (
username varchar(30),
admin boolean
);
CREATE TABLE
psycopgtest=# INSERT INTO users
(username, admin)
VALUES
('ran', true),
('haki', false);
INSERT 0 2
psycopgtest=# SELECT * FROM users;
username | admin
----------+-------
ran | t
haki | f
(2 rows)
Le tableau comporte deux colonnes :username
et admin
. L'admin
colonne indique si un utilisateur dispose ou non de privilèges administratifs. Votre objectif est de cibler l'admin
champ et essayez 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 façon de procéder, consultez Python Virtual Environments :A Primer.
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 a un adaptateur principal :
Base de données | Adaptateur |
---|---|
PostgreSQL | Psycopg |
SQLite | sqlite3 |
Oracle | cx_oracle |
MySql | MySQLdb |
Pour vous connecter à une base de données PostgreSQL, vous devez installer Psycopg, qui est l'adaptateur le plus populaire pour PostgreSQL en Python. Django ORM l'utilise par défaut, et il est également pris en charge par SQLAlchemy.
Dans votre terminal, activez l'environnement virtuel et utilisez pip
pour installer psycopg
:
(~/src/psycopgtest) $ source venv/bin/activate
(~/src/psycopgtest) $ python -m pip install psycopg2>=2.8.0
Collecting psycopg2
Using cached https://....
psycopg2-2.8.2.tar.gz
Installing collected packages: psycopg2
Running setup.py install for psycopg2 ... done
Successfully installed psycopg2-2.8.2
Vous êtes maintenant prêt à créer une connexion à votre base de données. Voici le début de votre script Python :
import psycopg2
connection = psycopg2.connect(
host="localhost",
database="psycopgtest",
user="postgres",
password=None,
)
connection.set_session(autocommit=True)
Vous avez utilisé psycopg2.connect()
pour créer la connexion. Cette fonction accepte les arguments suivants :
-
host
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 machine locale, oulocalhost
. -
database
est le nom de la base de données à laquelle se connecter. Vous souhaitez vous connecter à la base de données que vous avez créée précédemment,psycopgtest
. -
user
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, vous passez donc l'utilisateurpostgres
. -
password
est le mot de passe de la personne que vous avez spécifiée dansuser
. 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 établi la connexion, vous avez configuré la session avec autocommit=True
. Activation de autocommit
signifie que vous n'aurez pas à gérer manuellement les transactions en émettant un commit
ou rollback
. C'est le comportement par défaut dans la plupart des ORM. Vous utilisez également ce comportement ici afin de pouvoir vous concentrer sur la composition de requêtes SQL au lieu de gérer les transactions.
Remarque : Les utilisateurs de Django peuvent obtenir l'instance de la connexion utilisée par l'ORM à partir de django.db.connection
:
from django.db import connection
Exécuter une requête
Maintenant que vous êtes connecté à la base de données, vous êtes prêt à exécuter une requête :
>>>>>> with connection.cursor() as cursor:
... cursor.execute('SELECT COUNT(*) FROM users')
... result = cursor.fetchone()
... print(result)
(2,)
Vous avez utilisé la connection
objet pour créer un cursor
. Tout comme un fichier en Python, cursor
est implémenté en tant que gestionnaire de contexte. Lorsque vous créez le contexte, un cursor
est ouvert pour que vous puissiez l'utiliser pour envoyer des commandes à la base de données. A la sortie du contexte, le cursor
se ferme et vous ne pouvez plus l'utiliser.
Remarque : Pour en savoir plus sur les gestionnaires de contexte, consultez Python Context Managers et la déclaration "with".
Dans le contexte, vous avez utilisé cursor
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 dans les users
table. Pour récupérer le résultat de la requête, vous avez exécuté cursor.fetchone()
et a reçu un tuple. Étant donné que la requête ne peut renvoyer qu'un seul résultat, vous avez utilisé fetchone()
. Si la requête renvoyait plus d'un résultat, vous auriez besoin soit de parcourir cursor
ou utilisez l'un des autres fetch*
méthodes.
Utilisation des paramètres de requête en SQL
Dans la section précédente, vous avez créé une base de données, établi une connexion avec celle-ci et exécuté une requête. La requête que vous avez utilisée était statique . En d'autres termes, il n'avait aucun paramètre . 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 un administrateur ou non. is_admin()
accepte un nom d'utilisateur et renvoie le statut d'administrateur de cet utilisateur :
# BAD EXAMPLE. DON'T DO THIS!
def is_admin(username: str) -> bool:
with connection.cursor() as cursor:
cursor.execute("""
SELECT
admin
FROM
users
WHERE
username = '%s'
""" % username)
result = cursor.fetchone()
admin, = result
return admin
Cette fonction exécute une requête pour récupérer la valeur du admin
colonne pour un nom d'utilisateur donné. Vous avez utilisé fetchone()
pour retourner 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'utilisateur :
>>> is_admin('haki')
False
>>> is_admin('ran')
True
Jusqu'ici tout va bien. La fonction a renvoyé le résultat attendu pour les deux utilisateurs. Mais qu'en est-il de l'utilisateur inexistant ? Jetez un œil à ce traceback Python :
>>>>>> is_admin('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 12, in is_admin
TypeError: cannot unpack non-iterable NoneType object
Lorsque l'utilisateur n'existe pas, une TypeError
est relevé. C'est parce que .fetchone()
renvoie None
lorsqu'aucun résultat n'est trouvé, et déballer None
génère une TypeError
. Le seul endroit où vous pouvez décompresser un tuple est celui où vous remplissez admin
à partir du result
.
Pour gérer les utilisateurs inexistants, créez un cas spécial pour quand result
est None
:
# BAD EXAMPLE. DON'T DO THIS!
def is_admin(username: str) -> bool:
with connection.cursor() as cursor:
cursor.execute("""
SELECT
admin
FROM
users
WHERE
username = '%s'
""" % username)
result = cursor.fetchone()
if result is None:
# User does not exist
return False
admin, = result
return admin
Ici, vous avez ajouté un cas particulier pour la gestion de None
. Si username
n'existe pas, alors la fonction doit retourner False
. Encore une fois, testez la fonction sur certains utilisateurs :
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
Génial! La fonction peut désormais également gérer les noms d'utilisateur inexistants.
Exploitation des paramètres de requête avec Python SQL Injection
Dans l'exemple précédent, vous avez utilisé l'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, il y a quelque chose que vous avez peut-être oublié au cours de ce processus.
Repensez au username
argument que vous avez passé à is_admin()
. Que représente exactement cette variable ? Vous pourriez supposer que username
est juste une chaîne qui représente le nom d'un utilisateur réel. Comme vous êtes sur le point de le voir, cependant, un intrus peut facilement exploiter ce type de surveillance 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("'; select true; --")
True
Attendez… Que s'est-il passé ?
Reprenons la mise en œuvre. Imprimez la requête en cours d'exécution dans la base de données :
>>>>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'
Le texte résultant contient trois déclarations. Pour comprendre exactement comment fonctionne l'injection Python SQL, vous devez inspecter chaque partie individuellement. La première déclaration est la suivante :
select admin from users where username = '';
Ceci est votre requête prévue. Le point-virgule (;
) termine la requête, donc le résultat de cette requête n'a pas d'importance. Vient ensuite la deuxième déclaration :
select true;
Cette déclaration a été construite par l'intrus. Il est conçu pour toujours renvoyer True
.
Enfin, vous voyez ce petit bout 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 pourriez avoir mis après le dernier espace réservé en commentaire.
Lorsque vous exécutez la fonction avec cet argument, elle renverra toujours True
. Si, par exemple, vous utilisez cette fonction dans votre page de connexion, un intrus pourrait se connecter avec le nom d'utilisateur '; select true; --
, et l'accès leur sera accordé.
Si vous pensez que c'est mauvais, ça 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 dans la base de données :
>>>>>> is_admin('haki')
False
>>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --")
True
>>> is_admin('haki')
True
Décomposons-le à nouveau :
';
Cet extrait termine la requête, comme dans l'injection précédente. La déclaration suivante est la suivante :
update users set admin = 'true' where username = 'haki';
Cette section met à jour admin
à true
pour l'utilisateur haki
.
Enfin, il y a cet extrait de code :
select true; --
Comme dans l'exemple précédent, cette pièce renvoie true
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 administrateur :
psycopgtest=# select * from users;
username | admin
----------+-------
ran | t
haki | t
(2 rows)
L'intrus n'a plus besoin d'utiliser le hack. Ils peuvent simplement se connecter avec le nom d'utilisateur haki
. (Si l'intrus vraiment voulait causer du tort, alors ils pourraient même émettre un DROP DATABASE
commande.)
Avant d'oublier, restaurez haki
retour à son état d'origine :
psycopgtest=# update users set admin = false where username = 'haki';
UPDATE 1
Pourquoi cela se produit-il donc? Eh bien, que savez-vous du username
argument? Vous savez que cela devrait être une chaîne représentant le nom d'utilisateur, mais vous ne vérifiez pas ou n'appliquez pas cette assertion. Cela peut être dangereux ! C'est exactement ce que recherchent les attaquants lorsqu'ils tentent de pirater votre système.
Élaboration de paramètres de requête sécurisés
Dans la section précédente, vous avez vu comment un intrus peut exploiter votre système et obtenir des autorisations d'administrateur en utilisant une chaîne soigneusement conçue. Le problème était que vous autorisiez 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 qu'une entrée utilisateur est utilisée 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 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 vouliez pour username
à utiliser 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 :
>>>>>> # BAD EXAMPLE. DON'T DO THIS!
>>> username = username.replace("'", "''")
Ceci n'est qu'un exemple. Il y a beaucoup de caractères spéciaux et de scénarios à prendre en compte lorsque vous essayez 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 Python SQL en utilisant des paramètres de requête . Ceux-ci sont utilisés à la place de l'interpolation de chaîne simple pour composer une requête avec des paramètres.
Remarque : Différents adaptateurs, bases de données et langages de programmation font référence à des paramètres de requête sous des noms différents. Les noms communs incluent les variables de liaison , variables de remplacement , et variables de substitution .
Maintenant que vous avez une meilleure compréhension de 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 :
1def is_admin(username: str) -> bool:
2 with connection.cursor() as cursor:
3 cursor.execute("""
4 SELECT
5 admin
6 FROM
7 users
8 WHERE
9 username = %(username)s
10 """, {
11 'username': username
12 })
13 result = cursor.fetchone()
14
15 if result is None:
16 # User does not exist
17 return False
18
19 admin, = result
20 return admin
Voici ce qui est différent dans cet exemple :
-
À la ligne 9, vous avez utilisé un paramètre nommé
username
pour indiquer où le nom d'utilisateur doit aller. Remarquez comment le paramètreusername
n'est plus entouré de guillemets simples. -
À la ligne 11, vous avez passé la valeur de
username
comme deuxième argument decursor.execute()
. La connexion utilisera le type et la valeur deusername
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')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
>>> is_admin("'; select true; --")
False
Étonnante! 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 execute()
:
>>> with connection.cursor() as cursor:
... cursor.execute("""
... SELECT
... admin
... FROM
... users
... WHERE
... username = %(username)s
... """, {
... 'username': "'; select true; --"
... })
... print(cursor.query.decode('utf-8'))
SELECT
admin
FROM
users
WHERE
username = '''; select true; --'
La connexion a traité la valeur de username
sous forme de chaîne et a échappé tous les caractères qui pourraient terminer la chaîne et introduire l'injection Python SQL.
Passer des paramètres de requête sécurisés
Les adaptateurs de base de données offrent généralement plusieurs façons de transmettre les 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 bénéficier de l'utilisation d'autres options.
Jetons un coup d'œil à certaines des bonnes et des mauvaises façons d'utiliser les paramètres de requête. Le bloc de code suivant montre les types de requêtes que vous voudrez éviter :
# BAD EXAMPLES. DON'T DO THIS!
cursor.execute("SELECT admin FROM users WHERE username = '" + username + '");
cursor.execute("SELECT admin FROM users WHERE username = '%s' % username);
cursor.execute("SELECT admin FROM users WHERE username = '{}'".format(username));
cursor.execute(f"SELECT admin FROM users WHERE username = '{username}'");
Chacune de ces instructions transmet username
du client directement à la base de données, sans effectuer aucune sorte de contrôle ou de validation. Ce type de code est mûr pour inviter l'injection Python SQL.
En revanche, ces types de requêtes doivent pouvoir être exécutés en toute sécurité :
# SAFE EXAMPLES. DO THIS!
cursor.execute("SELECT admin FROM users WHERE username = %s'", (username, ));
cursor.execute("SELECT admin FROM users WHERE username = %(username)s", {'username': username});
Dans ces déclarations, username
est passé en tant que paramètre nommé. Maintenant, la base de données utilisera le type et la valeur spécifiés de username
lors de l'exécution de la requête, offrant une protection contre l'injection Python SQL.
Utiliser 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, une requête dans laquelle le paramètre est autre chose, comme un nom de table ou de colonne ?
Inspiré 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 :
# BAD EXAMPLE. DON'T DO THIS!
def count_rows(table_name: str) -> int:
with connection.cursor() as cursor:
cursor.execute("""
SELECT
count(*)
FROM
%(table_name)s
""", {
'table_name': table_name,
})
result = cursor.fetchone()
rowcount, = result
return rowcount
Essayez d'exécuter la fonction sur votre table d'utilisateurs :
>>>Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 9, in count_rows
psycopg2.errors.SyntaxError: syntax error at or near "'users'"
LINE 5: 'users'
^
La commande n'a pas réussi à 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 chaîne simple. C'est là qu'intervient la composition SQL.
Vous savez déjà qu'il n'est pas sûr d'utiliser l'interpolation de chaîne pour composer du SQL. Heureusement, Psycopg fournit un module appelé psycopg.sql
pour vous aider à composer en toute sécurité des requêtes SQL. Réécrivons la fonction en utilisant psycopg.sql.SQL()
:
from psycopg2 import sql
def count_rows(table_name: str) -> int:
with connection.cursor() as cursor:
stmt = sql.SQL("""
SELECT
count(*)
FROM
{table_name}
""").format(
table_name = sql.Identifier(table_name),
)
cursor.execute(stmt)
result = cursor.fetchone()
rowcount, = result
return rowcount
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()
pour annoter la valeur de l'argument table_name
. (Un identifiant est un nom de colonne ou de table.)
Remarque : Utilisateurs du package populaire django-debug-toolbar
peut obtenir une erreur dans le panneau SQL pour les requêtes composées avec psycopg.sql.SQL()
. Un correctif est attendu pour la version 2.0.
Maintenant, essayez d'exécuter la fonction sur les users
tableau :
>>> count_rows('users')
2
Génial! Voyons ensuite ce qui se passe lorsque la table n'existe pas :
>>>>>> count_rows('foo')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in count_rows
psycopg2.errors.UndefinedTable: relation "foo" does not exist
LINE 5: "foo"
^
La fonction lance la UndefinedTable
exception. Dans les étapes suivantes, vous utiliserez cette exception pour indiquer que votre fonction est à l'abri d'une attaque par injection Python SQL.
Remarque : L'exception UndefinedTable
a été ajouté dans la version 2.8 de psycopg2. Si vous travaillez avec une version antérieure de Psycopg, vous obtiendrez une exception différente.
Pour tout mettre ensemble, ajoutez une option pour compter les lignes dans le tableau jusqu'à une certaine limite. Cette fonctionnalité peut être utile pour les très grandes tables. Pour implémenter cela, ajoutez un LIMIT
clause à la requête, ainsi que les paramètres de requête pour la valeur de la limite :
from psycopg2 import sql
def count_rows(table_name: str, limit: int) -> int:
with connection.cursor() as cursor:
stmt = sql.SQL("""
SELECT
COUNT(*)
FROM (
SELECT
1
FROM
{table_name}
LIMIT
{limit}
) AS limit_query
""").format(
table_name = sql.Identifier(table_name),
limit = sql.Literal(limit),
)
cursor.execute(stmt)
result = cursor.fetchone()
rowcount, = result
return rowcount
Dans ce bloc de code, vous avez annoté limit
en utilisant sql.Literal()
. Comme dans l'exemple précédent, psycopg
liera tous les paramètres de requête en tant que littéraux lors de l'utilisation de l'approche simple. Cependant, lors de l'utilisation de sql.SQL()
, vous devez annoter explicitement chaque paramètre en utilisant soit sql.Identifier()
ou sql.Literal()
.
Remarque : Malheureusement, la spécification de l'API Python ne traite pas de la liaison des identifiants, uniquement des littéraux. Psycopg est le seul adaptateur populaire qui a ajouté la possibilité de composer en toute sécurité SQL avec des littéraux et des identifiants. Ce fait rend encore plus important de porter une attention particulière lors de la liaison des identifiants.
Exécutez la fonction pour vous assurer qu'elle fonctionne :
>>>>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2
Maintenant que vous voyez que la fonction fonctionne, assurez-vous qu'elle est également sécurisée :
>>>>>> count_rows("(select 1) as foo; update users set admin = true where name = 'haki'; --", 1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 18, in count_rows
psycopg2.errors.UndefinedTable: relation "(select 1) as foo; update users set admin = true where name = '" does not exist
LINE 8: "(select 1) as foo; update users set adm...
^
Cette trace montre que psycopg
a échappé la valeur et la base de données l'a traitée comme un nom de table. Puisqu'une table avec ce nom n'existe pas, une UndefinedTable
une exception a été levée et vous n'avez pas été piraté !
Conclusion
Vous avez implémenté avec succès une fonction qui compose du SQL dynamique sans mettre votre système en danger pour l'injection Python SQL ! Vous avez utilisé à la fois des littéraux et des identifiants dans votre requête sans compromettre la sécurité.
Vous avez appris :
- Qu'est-ce que l'injection Python SQL ? est et comment il peut être exploité
- Comment empêcher l'injection Python SQL en utilisant des paramètres de requête
- Comment composer des instructions SQL en toute sécurité qui utilisent des littéraux et des identifiants comme paramètres
Vous êtes maintenant en mesure de créer des programmes capables de résister aux attaques de l'extérieur. Allez-y et déjouez les pirates !