Alle paar Jahre listet das Open Web Application Security Project (OWASP) die kritischsten Sicherheitsrisiken für Webanwendungen auf. Seit dem ersten Bericht standen immer die Injektionsrisiken im Vordergrund. Unter allen Injektionstypen SQL-Injection ist einer der häufigsten Angriffsvektoren und wohl der gefährlichste. Da Python eine der beliebtesten Programmiersprachen der Welt ist, ist es wichtig zu wissen, wie man sich vor Python SQL Injection schützt.
In diesem Tutorial lernen Sie:
- Welche Python-SQL-Injektion ist und wie man es verhindert
- Wie man Abfragen erstellt mit sowohl Literalen als auch Bezeichnern als Parameter
- Abfragen sicher ausführen in einer Datenbank
Dieses Tutorial ist für Benutzer aller Datenbank-Engines geeignet . Die Beispiele hier verwenden PostgreSQL, aber die Ergebnisse können in anderen Datenbankverwaltungssystemen (wie SQLite, MySQL, Microsoft SQL Server, Oracle usw.) reproduziert werden.
Kostenloser Bonus: 5 Gedanken zur Python-Beherrschung, ein kostenloser Kurs für Python-Entwickler, der Ihnen den Fahrplan und die Denkweise zeigt, die Sie benötigen, um Ihre Python-Kenntnisse auf die nächste Stufe zu heben.
Python SQL Injection verstehen
SQL-Injection-Angriffe sind eine so häufige Sicherheitslücke, dass die legendäre xkcd webcomic hat ihm einen Comic gewidmet:
Das Generieren und Ausführen von SQL-Abfragen ist eine häufige Aufgabe. Unternehmen auf der ganzen Welt machen jedoch oft schreckliche Fehler, wenn es darum geht, SQL-Anweisungen zu erstellen. Während die ORM-Schicht normalerweise SQL-Abfragen erstellt, müssen Sie manchmal Ihre eigenen schreiben.
Wenn Sie Python verwenden, um diese Abfragen direkt in einer Datenbank auszuführen, besteht die Möglichkeit, dass Sie Fehler machen, die Ihr System gefährden könnten. In diesem Tutorial erfahren Sie, wie Sie erfolgreich Funktionen implementieren, die dynamische SQL-Abfragen ohne erstellen Ihr System einem Risiko für Python-SQL-Einschleusung aussetzen.
Einrichten einer Datenbank
Zu Beginn richten Sie eine neue PostgreSQL-Datenbank ein und füllen sie mit Daten. Während des gesamten Tutorials verwenden Sie diese Datenbank, um aus erster Hand zu sehen, wie Python SQL Injection funktioniert.
Erstellen einer Datenbank
Öffnen Sie zuerst Ihre Shell und erstellen Sie eine neue PostgreSQL-Datenbank, die dem Benutzer postgres
gehört :
$ createdb -O postgres psycopgtest
Hier haben Sie die Kommandozeilenoption -O
verwendet um den Besitzer der Datenbank auf den Benutzer postgres
zu setzen . Sie haben auch den Namen der Datenbank angegeben, der psycopgtest
ist .
Hinweis: postgres
ist ein spezieller Benutzer , die Sie normalerweise für Verwaltungsaufgaben reservieren würden, aber für dieses Tutorial ist es in Ordnung, postgres
zu verwenden . In einem realen System sollten Sie jedoch einen separaten Benutzer als Eigentümer der Datenbank erstellen.
Ihre neue Datenbank ist startklar! Sie können sich mit psql
damit verbinden :
$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.
Sie sind jetzt mit der Datenbank psycopgtest
verbunden als Benutzer postgres
. Dieser Benutzer ist auch der Eigentümer der Datenbank, sodass Sie Leseberechtigungen für jede Tabelle in der Datenbank haben.
Erstellen einer Tabelle mit Daten
Als nächstes müssen Sie eine Tabelle mit einigen Benutzerinformationen erstellen und ihr Daten hinzufügen:
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)
Die Tabelle hat zwei Spalten:username
und admin
. Der admin
Spalte gibt an, ob ein Benutzer Administratorrechte hat oder nicht. Ihr Ziel ist es, den admin
anzusprechen und versuchen, es zu missbrauchen.
Einrichten einer virtuellen Python-Umgebung
Nachdem Sie nun über eine Datenbank verfügen, ist es an der Zeit, Ihre Python-Umgebung einzurichten. Eine Schritt-für-Schritt-Anleitung dazu finden Sie unter Python Virtual Environments:A Primer.
Erstellen Sie Ihre virtuelle Umgebung in einem neuen Verzeichnis:
(~/src) $ mkdir psycopgtest
(~/src) $ cd psycopgtest
(~/src/psycopgtest) $ python3 -m venv venv
Nachdem Sie diesen Befehl ausgeführt haben, wird ein neues Verzeichnis namens venv
wird erstellt. Dieses Verzeichnis speichert alle Pakete, die Sie in der virtuellen Umgebung installieren.
Verbindung zur Datenbank herstellen
Um eine Verbindung zu einer Datenbank in Python herzustellen, benötigen Sie einen Datenbankadapter . Die meisten Datenbankadapter folgen Version 2.0 der Python-Datenbank-API-Spezifikation PEP 249. Jede größere Datenbank-Engine hat einen führenden Adapter:
Datenbank | Adapter |
---|---|
PostgreSQL | Psychopg |
SQLite | sqlite3 |
Oracle | cx_oracle |
MySql | MySQLdb |
Um eine Verbindung zu einer PostgreSQL-Datenbank herzustellen, müssen Sie Psycopg installieren, den beliebtesten Adapter für PostgreSQL in Python. Django ORM verwendet es standardmäßig und wird auch von SQLAlchemy unterstützt.
Aktivieren Sie in Ihrem Terminal die virtuelle Umgebung und verwenden Sie pip
um psycopg
zu installieren :
(~/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
Jetzt können Sie eine Verbindung zu Ihrer Datenbank herstellen. Hier ist der Anfang Ihres Python-Skripts:
import psycopg2
connection = psycopg2.connect(
host="localhost",
database="psycopgtest",
user="postgres",
password=None,
)
connection.set_session(autocommit=True)
Sie haben psycopg2.connect()
verwendet um die Verbindung herzustellen. Diese Funktion akzeptiert die folgenden Argumente:
-
host
ist die IP-Adresse oder der DNS des Servers, auf dem sich Ihre Datenbank befindet. In diesem Fall ist der Host Ihr lokaler Rechner oderlocalhost
. -
database
ist der Name der Datenbank, zu der eine Verbindung hergestellt werden soll. Sie möchten sich mit der zuvor erstellten Datenbankpsycopgtest
verbinden . -
user
ist ein Benutzer mit Berechtigungen für die Datenbank. In diesem Fall möchten Sie sich als Besitzer mit der Datenbank verbinden, also übergeben Sie den Benutzerpostgres
. -
password
ist das Passwort für denjenigen, den Sie inuser
angegeben haben . In den meisten Entwicklungsumgebungen können sich Benutzer ohne Passwort mit der lokalen Datenbank verbinden.
Nach dem Verbindungsaufbau haben Sie die Sitzung mit autocommit=True
konfiguriert . autocommit
aktivieren bedeutet, dass Sie Transaktionen nicht manuell verwalten müssen, indem Sie ein commit
ausgeben oder rollback
. Dies ist das Standardverhalten in den meisten ORMs. Sie verwenden dieses Verhalten auch hier, damit Sie sich auf das Erstellen von SQL-Abfragen konzentrieren können, anstatt Transaktionen zu verwalten.
Hinweis: Django-Benutzer können die Instanz der vom ORM verwendeten Verbindung von django.db.connection
abrufen :
from django.db import connection
Ausführen einer Abfrage
Da Sie nun eine Verbindung zur Datenbank haben, können Sie eine Abfrage ausführen:
>>>>>> with connection.cursor() as cursor:
... cursor.execute('SELECT COUNT(*) FROM users')
... result = cursor.fetchone()
... print(result)
(2,)
Sie haben die connection
verwendet -Objekt, um einen cursor
zu erstellen . Genau wie eine Datei in Python, cursor
ist als Kontextmanager implementiert. Wenn Sie den Kontext erstellen, ein cursor
wird geöffnet, damit Sie Befehle an die Datenbank senden können. Wenn der Kontext beendet wird, wird der cursor
wird geschlossen und Sie können es nicht mehr verwenden.
Hinweis: Weitere Informationen zu Kontextmanagern finden Sie unter Python-Kontextmanager und der „with“-Anweisung.
Innerhalb des Kontexts haben Sie cursor
verwendet um eine Abfrage auszuführen und die Ergebnisse abzurufen. In diesem Fall haben Sie eine Abfrage ausgegeben, um die Zeilen in users
zu zählen Tisch. Um das Ergebnis der Abfrage abzurufen, haben Sie cursor.fetchone()
ausgeführt und erhielt ein Tupel. Da die Abfrage nur ein Ergebnis zurückgeben kann, haben Sie fetchone()
verwendet . Wenn die Abfrage mehr als ein Ergebnis zurückgeben würde, müssten Sie entweder über cursor
iterieren oder verwenden Sie einen der anderen fetch*
Methoden.
Verwendung von Abfrageparametern in SQL
Im vorherigen Abschnitt haben Sie eine Datenbank erstellt, eine Verbindung zu ihr hergestellt und eine Abfrage ausgeführt. Die von Ihnen verwendete Abfrage war statisch . Mit anderen Worten, es hatte keine Parameter . Jetzt beginnen Sie damit, Parameter in Ihren Abfragen zu verwenden.
Zuerst werden Sie eine Funktion implementieren, die überprüft, ob ein Benutzer ein Administrator ist oder nicht. is_admin()
akzeptiert einen Benutzernamen und gibt den Administratorstatus dieses Benutzers zurück:
# 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
Diese Funktion führt eine Abfrage aus, um den Wert von admin
abzurufen Spalte für einen bestimmten Benutzernamen. Sie haben fetchone()
verwendet um ein Tupel mit einem einzigen Ergebnis zurückzugeben. Anschließend haben Sie dieses Tupel in die Variable admin
entpackt . Um Ihre Funktion zu testen, überprüfen Sie einige Benutzernamen:
>>> is_admin('haki')
False
>>> is_admin('ran')
True
So weit, ist es gut. Die Funktion hat für beide Benutzer das erwartete Ergebnis zurückgegeben. Aber was ist mit nicht existierenden Benutzern? Sehen Sie sich dieses Python-Traceback an:
>>>>>> 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
Wenn der Benutzer nicht existiert, ein TypeError
wird angehoben. Das liegt daran, dass .fetchone()
gibt None
zurück wenn keine Ergebnisse gefunden werden, und Entpacken von None
löst einen TypeError
aus . Der einzige Ort, an dem Sie ein Tupel entpacken können, ist dort, wo Sie admin
füllen aus result
.
Um mit nicht vorhandenen Benutzern umzugehen, erstellen Sie einen Sonderfall für when result
ist 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
Hier haben Sie einen Sonderfall für die Behandlung von None
hinzugefügt . Wenn username
nicht existiert, sollte die Funktion False
zurückgeben . Testen Sie die Funktion noch einmal an einigen Benutzern:
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
Toll! Die Funktion kann jetzt auch mit nicht existierenden Benutzernamen umgehen.
Ausnutzen von Abfrageparametern mit Python SQL Injection
Im vorherigen Beispiel haben Sie eine Zeichenfolgeninterpolation verwendet, um eine Abfrage zu generieren. Anschließend haben Sie die Abfrage ausgeführt und die resultierende Zeichenfolge direkt an die Datenbank gesendet. Es gibt jedoch etwas, das Sie während dieses Vorgangs möglicherweise übersehen haben.
Denken Sie an den username
zurück Argument, das Sie an is_admin()
übergeben haben . Was genau repräsentiert diese Variable? Sie könnten davon ausgehen, dass username
ist nur eine Zeichenfolge, die den Namen eines tatsächlichen Benutzers darstellt. Wie Sie jedoch gleich sehen werden, kann ein Eindringling diese Art von Versehen leicht ausnutzen und großen Schaden anrichten, indem er eine Python-SQL-Injection durchführt.
Versuchen Sie zu überprüfen, ob der folgende Benutzer ein Administrator ist oder nicht:
>>>>>> is_admin("'; select true; --")
True
Moment… Was ist gerade passiert?
Schauen wir uns noch einmal die Umsetzung an. Drucken Sie die aktuelle Abfrage aus, die in der Datenbank ausgeführt wird:
>>>>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'
Der resultierende Text enthält drei Aussagen. Um genau zu verstehen, wie Python SQL Injection funktioniert, müssen Sie jeden Teil einzeln untersuchen. Die erste Anweisung lautet wie folgt:
select admin from users where username = '';
Dies ist Ihre beabsichtigte Abfrage. Das Semikolon (;
) beendet die Abfrage, sodass das Ergebnis dieser Abfrage keine Rolle spielt. Als nächstes folgt die zweite Anweisung:
select true;
Diese Aussage wurde vom Eindringling konstruiert. Es ist so konzipiert, dass es immer True
zurückgibt .
Zuletzt sehen Sie diesen kurzen Code:
--'
Dieser Ausschnitt entschärft alles, was danach kommt. Der Eindringling hat das Kommentarsymbol (--
), um alles, was Sie möglicherweise nach dem letzten Platzhalter eingefügt haben, in einen Kommentar umzuwandeln.
Wenn Sie die Funktion mit diesem Argument ausführen, gibt sie immer True
zurück . Wenn Sie diese Funktion beispielsweise auf Ihrer Anmeldeseite verwenden, könnte sich ein Eindringling mit dem Benutzernamen '; select true; --
, und ihnen wird Zugriff gewährt.
Wenn Sie denken, dass dies schlecht ist, könnte es noch schlimmer werden! Eindringlinge, die Ihre Tabellenstruktur kennen, können mit Python SQL Injection dauerhaften Schaden anrichten. Beispielsweise kann der Eindringling eine Update-Anweisung einfügen, um die Informationen in der Datenbank zu ändern:
>>>>>> is_admin('haki')
False
>>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --")
True
>>> is_admin('haki')
True
Lassen Sie es uns noch einmal aufschlüsseln:
';
Dieses Snippet beendet die Abfrage, genau wie bei der vorherigen Injektion. Die nächste Anweisung lautet wie folgt:
update users set admin = 'true' where username = 'haki';
Dieser Abschnitt aktualisiert admin
auf true
für Benutzer haki
.
Schließlich gibt es dieses Code-Snippet:
select true; --
Wie im vorherigen Beispiel gibt dieses Stück true
zurück und kommentiert alles, was darauf folgt.
Warum ist das schlimmer? Nun, wenn es dem Eindringling gelingt, die Funktion mit dieser Eingabe auszuführen, dann Benutzer haki
wird Administrator:
psycopgtest=# select * from users;
username | admin
----------+-------
ran | t
haki | t
(2 rows)
Der Eindringling muss den Hack nicht mehr verwenden. Sie können sich einfach mit dem Benutzernamen haki
anmelden . (Wenn der Eindringling wirklich Schaden anrichten wollten, dann könnten sie sogar ein DROP DATABASE
ausgeben Befehl.)
Bevor Sie es vergessen, stellen Sie haki
wieder her zurück in den ursprünglichen Zustand:
psycopgtest=# update users set admin = false where username = 'haki';
UPDATE 1
Also, warum passiert das? Nun, was wissen Sie über den username
Streit? Sie wissen, dass es sich um eine Zeichenfolge handeln sollte, die den Benutzernamen darstellt, aber Sie überprüfen oder erzwingen diese Behauptung nicht wirklich. Das kann gefährlich werden! Das ist genau das, wonach Angreifer suchen, wenn sie versuchen, Ihr System zu hacken.
Erstellen sicherer Abfrageparameter
Im vorherigen Abschnitt haben Sie gesehen, wie ein Eindringling Ihr System ausnutzen und Administratorrechte erlangen kann, indem er eine sorgfältig erstellte Zeichenfolge verwendet. Das Problem war, dass Sie zugelassen haben, dass der vom Client übergebene Wert direkt an die Datenbank ausgeführt wird, ohne irgendeine Art von Überprüfung oder Validierung durchzuführen. SQL-Injections beruhen auf dieser Art von Schwachstelle.
Jedes Mal, wenn Benutzereingaben in einer Datenbankabfrage verwendet werden, besteht eine mögliche Schwachstelle für SQL-Injection. Der Schlüssel zum Verhindern der Python-SQL-Einschleusung besteht darin, sicherzustellen, dass der Wert wie vom Entwickler beabsichtigt verwendet wird. Im vorherigen Beispiel war username
beabsichtigt als String verwendet werden. In Wirklichkeit wurde es als reines SQL-Statement verwendet.
Um sicherzustellen, dass die Werte wie beabsichtigt verwendet werden, müssen Sie escapen der Wert. Um beispielsweise zu verhindern, dass Eindringlinge rohes SQL anstelle eines Zeichenfolgenarguments einfügen, können Sie Anführungszeichen maskieren:
>>>>>> # BAD EXAMPLE. DON'T DO THIS!
>>> username = username.replace("'", "''")
Dies ist nur ein Beispiel. Es gibt viele Sonderzeichen und Szenarien, an die Sie denken müssen, wenn Sie versuchen, die Python-SQL-Injection zu verhindern. Glücklicherweise verfügen moderne Datenbankadapter über integrierte Tools zum Verhindern der Python-SQL-Einschleusung mithilfe von Abfrageparametern . Diese werden anstelle der einfachen Zeichenfolgeninterpolation verwendet, um eine Abfrage mit Parametern zu erstellen.
Hinweis: Verschiedene Adapter, Datenbanken und Programmiersprachen beziehen sich auf Abfrageparameter mit unterschiedlichen Namen. Zu den gebräuchlichen Namen gehören Bind-Variablen , Ersatzvariablen und Substitutionsvariablen .
Jetzt, da Sie die Schwachstelle besser verstehen, können Sie die Funktion mithilfe von Abfrageparametern anstelle von Zeichenfolgeninterpolation umschreiben:
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
Folgendes ist in diesem Beispiel anders:
-
In Zeile 9, Sie haben einen benannten Parameter
username
verwendet um anzugeben, wohin der Benutzername gehen soll. Beachten Sie, wie der Parameterusername
nicht mehr in einfache Anführungszeichen eingeschlossen. -
In Zeile 11, Sie haben den Wert von
username
übergeben als zweites Argument fürcursor.execute()
. Die Verbindung verwendet den Typ und Wert vonusername
beim Ausführen der Abfrage in der Datenbank.
Probieren Sie zum Testen dieser Funktion einige gültige und ungültige Werte aus, einschließlich der gefährlichen Zeichenfolge von vorhin:
>>>>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
>>> is_admin("'; select true; --")
False
Tolle! Die Funktion hat für alle Werte das erwartete Ergebnis zurückgegeben. Außerdem funktioniert die gefährliche Saite nicht mehr. Um zu verstehen, warum, können Sie die von execute()
generierte Abfrage überprüfen :
>>> 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; --'
Die Verbindung behandelte den Wert von username
als Zeichenfolge und maskiert alle Zeichen, die die Zeichenfolge beenden und eine Python-SQL-Einschleusung einführen könnten.
Übergeben sicherer Abfrageparameter
Datenbankadapter bieten normalerweise mehrere Möglichkeiten zum Übergeben von Abfrageparametern. Benannte Platzhalter sind normalerweise die besten für die Lesbarkeit, aber einige Implementierungen könnten von der Verwendung anderer Optionen profitieren.
Werfen wir einen kurzen Blick auf einige der richtigen und falschen Möglichkeiten zur Verwendung von Abfrageparametern. Der folgende Codeblock zeigt die Arten von Abfragen, die Sie vermeiden sollten:
# 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}'");
Jede dieser Anweisungen übergibt username
vom Client direkt in die Datenbank, ohne irgendeine Art von Überprüfung oder Validierung durchzuführen. Diese Art von Code ist reif für eine einladende Python-SQL-Injektion.
Im Gegensatz dazu sollten diese Arten von Abfragen für Sie sicher auszuführen sein:
# 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});
In diesen Anweisungen username
wird als benannter Parameter übergeben. Jetzt verwendet die Datenbank den angegebenen Typ und Wert von username
beim Ausführen der Abfrage und bietet Schutz vor Python-SQL-Einschleusung.
SQL-Komposition verwenden
Bisher haben Sie Parameter für Literale verwendet. Literale sind Werte wie Zahlen, Zeichenfolgen und Datumsangaben. Aber was ist, wenn Sie einen Anwendungsfall haben, der das Erstellen einer anderen Abfrage erfordert – eine, bei der der Parameter etwas anderes ist, wie z. B. ein Tabellen- oder Spaltenname?
Inspiriert vom vorherigen Beispiel implementieren wir eine Funktion, die den Namen einer Tabelle akzeptiert und die Anzahl der Zeilen in dieser Tabelle zurückgibt:
# 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
Versuchen Sie, die Funktion auf Ihrer Benutzertabelle auszuführen:
>>>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'
^
Der Befehl konnte die SQL nicht generieren. Wie Sie bereits gesehen haben, behandelt der Datenbankadapter die Variable als Zeichenfolge oder Literal. Ein Tabellenname ist jedoch keine einfache Zeichenfolge. Hier kommt die SQL-Komposition ins Spiel.
Sie wissen bereits, dass es nicht sicher ist, Zeichenfolgeninterpolation zum Erstellen von SQL zu verwenden. Glücklicherweise stellt Psycopg ein Modul namens psycopg.sql
bereit um Ihnen beim sicheren Erstellen von SQL-Abfragen zu helfen. Lassen Sie uns die Funktion mit psycopg.sql.SQL()
umschreiben :
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
Es gibt zwei Unterschiede in dieser Implementierung. Zuerst haben Sie sql.SQL()
verwendet um die Abfrage zu verfassen. Dann haben Sie sql.Identifier()
verwendet um den Argumentwert table_name
zu kommentieren . (Eine Kennung ist ein Spalten- oder Tabellenname.)
Hinweis: Benutzer des beliebten Pakets django-debug-toolbar
erhält möglicherweise einen Fehler im SQL-Fenster für Abfragen, die mit psycopg.sql.SQL()
erstellt wurden . Eine Fehlerbehebung wird für die Veröffentlichung in Version 2.0 erwartet.
Versuchen Sie nun, die Funktion für users
auszuführen Tabelle:
>>> count_rows('users')
2
Toll! Sehen wir uns als Nächstes an, was passiert, wenn die Tabelle nicht existiert:
>>>>>> 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"
^
Die Funktion wirft die UndefinedTable
Ausnahme. In den folgenden Schritten verwenden Sie diese Ausnahme als Hinweis darauf, dass Ihre Funktion vor einem Python-SQL-Injection-Angriff sicher ist.
Hinweis: Die Ausnahme UndefinedTable
wurde in psycopg2 Version 2.8 hinzugefügt. Wenn Sie mit einer früheren Version von Psycopg arbeiten, erhalten Sie eine andere Ausnahme.
Um alles zusammenzufassen, fügen Sie eine Option hinzu, um Zeilen in der Tabelle bis zu einem bestimmten Limit zu zählen. Diese Funktion kann für sehr große Tabellen nützlich sein. Um dies zu implementieren, fügen Sie ein LIMIT
hinzu -Klausel zur Abfrage, zusammen mit Abfrageparametern für den Grenzwert:
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
In diesem Codeblock haben Sie limit
kommentiert mit sql.Literal()
. Wie im vorherigen Beispiel, psycopg
bindet alle Abfrageparameter als Literale, wenn der einfache Ansatz verwendet wird. Bei Verwendung von sql.SQL()
jedoch müssen Sie jeden Parameter explizit mit sql.Identifier()
kommentieren oder sql.Literal()
.
Hinweis: Leider befasst sich die Python-API-Spezifikation nicht mit der Bindung von Bezeichnern, sondern nur mit Literalen. Psycopg ist der einzige populäre Adapter, der die Möglichkeit hinzugefügt hat, SQL mit Literalen und Bezeichnern sicher zu erstellen. Diese Tatsache macht es umso wichtiger, beim Binden von Identifikatoren genau darauf zu achten.
Führen Sie die Funktion aus, um sicherzustellen, dass sie funktioniert:
>>>>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2
Nachdem Sie nun sehen, dass die Funktion funktioniert, vergewissern Sie sich, dass sie auch sicher ist:
>>>>>> 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...
^
Diese Rückverfolgung zeigt, dass psycopg
hat den Wert maskiert und von der Datenbank als Tabellenname behandelt. Da eine Tabelle mit diesem Namen nicht existiert, wird eine UndefinedTable
Ausnahme wurde ausgelöst und Sie wurden nicht gehackt!
Schlussfolgerung
Sie haben erfolgreich eine Funktion implementiert, die dynamisches SQL ohne erstellt Setzen Sie Ihr System einem Risiko für die Python-SQL-Injektion aus! Sie haben in Ihrer Abfrage sowohl Literale als auch Bezeichner verwendet, ohne die Sicherheit zu gefährden.
Du hast gelernt:
- Welche Python-SQL-Injektion ist und wie sie ausgenutzt werden kann
- Wie man Python-SQL-Einschleusung verhindert Abfrageparameter verwenden
- So erstellen Sie sichere SQL-Anweisungen die Literale und Bezeichner als Parameter verwenden
Sie sind jetzt in der Lage, Programme zu erstellen, die Angriffen von außen standhalten. Gehen Sie hinaus und vereiteln Sie die Hacker!