Database
 sql >> Datenbank >  >> RDS >> Database

Verhindern von SQL-Injection-Angriffen mit Python

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 oder localhost .

  • database ist der Name der Datenbank, zu der eine Verbindung hergestellt werden soll. Sie möchten sich mit der zuvor erstellten Datenbank psycopgtest 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 Benutzer postgres .

  • password ist das Passwort für denjenigen, den Sie in user 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 Parameter username nicht mehr in einfache Anführungszeichen eingeschlossen.

  • In Zeile 11, Sie haben den Wert von username übergeben als zweites Argument für cursor.execute() . Die Verbindung verwendet den Typ und Wert von username 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!