Database
 sql >> Baza danych >  >> RDS >> Database

Zapobieganie atakom typu SQL Injection za pomocą Pythona

Co kilka lat projekt Open Web Application Security Project (OWASP) ocenia najbardziej krytyczne zagrożenia bezpieczeństwa aplikacji internetowych. Od pierwszego raportu ryzyko związane z wstrzyknięciem zawsze było na szczycie. Wśród wszystkich typów wstrzykiwania wstrzykiwanie SQL jest jednym z najczęstszych kierunków ataku i prawdopodobnie najniebezpieczniejszym. Ponieważ Python jest jednym z najpopularniejszych języków programowania na świecie, wiedza o tym, jak chronić się przed wstrzyknięciem Pythona SQL, ma kluczowe znaczenie.

W tym samouczku dowiesz się:

  • Jakie wstrzyknięcie Pythona SQL jest i jak temu zapobiec
  • Jak tworzyć zapytania z literałami i identyfikatorami jako parametrami
  • Jak bezpiecznie wykonywać zapytania w bazie danych

Ten samouczek jest odpowiedni dla użytkowników wszystkich silników baz danych . Poniższe przykłady używają PostgreSQL, ale wyniki można odtworzyć w innych systemach zarządzania bazami danych (takich jak SQLite, MySQL, Microsoft SQL Server, Oracle itd.).

Bezpłatny bonus: 5 Thoughts On Python Mastery, bezpłatny kurs dla programistów Pythona, który pokazuje mapę drogową i sposób myślenia, których będziesz potrzebować, aby przenieść swoje umiejętności Pythona na wyższy poziom.


Zrozumienie wstrzykiwania SQL w Pythonie

Ataki SQL Injection są tak powszechną luką w zabezpieczeniach, że legendarny xkcd webcomic poświęcił mu komiks:

Generowanie i wykonywanie zapytań SQL jest częstym zadaniem. Jednak firmy na całym świecie często popełniają okropne błędy, jeśli chodzi o tworzenie instrukcji SQL. Podczas gdy warstwa ORM zwykle tworzy zapytania SQL, czasami trzeba napisać własne.

Kiedy używasz Pythona do wykonywania tych zapytań bezpośrednio w bazie danych, istnieje ryzyko, że popełnisz błędy, które mogą zagrozić Twojemu systemowi. W tym samouczku dowiesz się, jak skutecznie zaimplementować funkcje, które tworzą dynamiczne zapytania SQL bez narażając swój system na ryzyko wstrzyknięcia Pythona SQL.



Konfigurowanie bazy danych

Na początek skonfigurujesz nową bazę danych PostgreSQL i wypełnisz ją danymi. W trakcie tego samouczka będziesz korzystać z tej bazy danych, aby osobiście przekonać się, jak działa wstrzykiwanie SQL w języku Python.


Tworzenie bazy danych

Najpierw otwórz swoją powłokę i utwórz nową bazę danych PostgreSQL, której właścicielem jest użytkownik postgres :

$ createdb -O postgres psycopgtest

Tutaj użyłeś opcji wiersza poleceń -O ustawić właściciela bazy danych na użytkownika postgres . Podałeś również nazwę bazy danych, która jest psycopgtest .

Uwaga: postgres jest użytkownikiem specjalnym , które normalnie zarezerwowałbyś dla zadań administracyjnych, ale w tym samouczku dobrze jest użyć postgres . Jednak w prawdziwym systemie powinieneś utworzyć osobnego użytkownika, który będzie właścicielem bazy danych.

Twoja nowa baza danych jest gotowa! Możesz się z nim połączyć za pomocą psql :

$ psql -U postgres -d psycopgtest
psql (11.2, server 10.5)
Type "help" for help.

Jesteś teraz połączony z bazą danych psycopgtest jako użytkownik postgres . Ten użytkownik jest również właścicielem bazy danych, więc będziesz mieć uprawnienia do odczytu każdej tabeli w bazie danych.



Tworzenie tabeli z danymi

Następnie musisz utworzyć tabelę z informacjami o użytkowniku i dodać do niej dane:

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)

Tabela ma dwie kolumny:username i admin . admin kolumna wskazuje, czy użytkownik ma uprawnienia administracyjne. Twoim celem jest dotarcie do admin pola i spróbuj go nadużyć.



Konfigurowanie wirtualnego środowiska Pythona

Teraz, gdy masz już bazę danych, nadszedł czas na skonfigurowanie środowiska Pythona. Aby uzyskać instrukcje krok po kroku, jak to zrobić, zapoznaj się z Python Virtual Environments:A Primer.

Utwórz swoje środowisko wirtualne w nowym katalogu:

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

Po uruchomieniu tego polecenia nowy katalog o nazwie venv zostanie utworzona. Ten katalog będzie przechowywać wszystkie pakiety, które zainstalujesz w środowisku wirtualnym.



Łączenie z bazą danych

Aby połączyć się z bazą danych w Pythonie, potrzebujesz adaptera bazy danych . Większość adapterów baz danych jest zgodna z wersją 2.0 specyfikacji Python Database API PEP 249. Każdy główny silnik bazy danych ma wiodący adapter:

Baza danych Adapter
PostgreSQL Psykopg
SQLite sqlite3
Wyrocznia cx_oracle
Mój SQL MySQLdb

Aby połączyć się z bazą danych PostgreSQL, musisz zainstalować Psycopg, który jest najpopularniejszym adapterem dla PostgreSQL w Pythonie. Django ORM używa go domyślnie i jest również obsługiwany przez SQLAlchemy.

W swoim terminalu aktywuj środowisko wirtualne i użyj pip zainstalować 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

Teraz jesteś gotowy do nawiązania połączenia ze swoją bazą danych. Oto początek twojego skryptu Pythona:

import psycopg2

connection = psycopg2.connect(
    host="localhost",
    database="psycopgtest",
    user="postgres",
    password=None,
)
connection.set_session(autocommit=True)

Użyłeś psycopg2.connect() aby utworzyć połączenie. Ta funkcja akceptuje następujące argumenty:

  • host to adres IP lub DNS serwera, na którym znajduje się Twoja baza danych. W tym przypadku hostem jest twój lokalny komputer lub localhost .

  • database to nazwa bazy danych, z którą chcesz się połączyć. Chcesz połączyć się z bazą danych, którą utworzyłeś wcześniej, psycopgtest .

  • user to użytkownik z uprawnieniami do bazy danych. W tym przypadku chcesz połączyć się z bazą danych jako właściciel, więc przekazujesz użytkownika postgres .

  • password to hasło dla osoby, którą określiłeś w user . W większości środowisk programistycznych użytkownicy mogą łączyć się z lokalną bazą danych bez hasła.

Po skonfigurowaniu połączenia skonfigurowałeś sesję za pomocą autocommit=True . Aktywacja autocommit oznacza, że ​​nie będziesz musiał ręcznie zarządzać transakcjami, wydając commit lub rollback . Jest to domyślne zachowanie większości ORM-ów. Używasz tego zachowania również tutaj, aby móc skupić się na tworzeniu zapytań SQL zamiast na zarządzaniu transakcjami.

Uwaga: Użytkownicy Django mogą pobrać instancję połączenia używanego przez ORM z django.db.connection :

from django.db import connection


Wykonywanie zapytania

Teraz, gdy masz połączenie z bazą danych, możesz wykonać zapytanie:

>>>
>>> with connection.cursor() as cursor:
...     cursor.execute('SELECT COUNT(*) FROM users')
...     result = cursor.fetchone()
... print(result)
(2,)

Użyłeś connection obiekt do utworzenia cursor . Podobnie jak plik w Pythonie, cursor jest zaimplementowany jako menedżer kontekstu. Podczas tworzenia kontekstu cursor jest otwarty do użycia do wysyłania poleceń do bazy danych. Po zakończeniu kontekstu cursor zamyka się i nie można już z niego korzystać.

Uwaga: Aby dowiedzieć się więcej o menedżerach kontekstu, zapoznaj się z menedżerami kontekstu Pythona i instrukcją „with”.

Wewnątrz kontekstu użyłeś cursor do wykonania zapytania i pobrania wyników. W tym przypadku wysłałeś zapytanie, aby policzyć wiersze w users stół. Aby pobrać wynik z zapytania, wykonałeś cursor.fetchone() i otrzymał krotkę. Ponieważ zapytanie może zwrócić tylko jeden wynik, użyłeś fetchone() . Jeśli zapytanie miałoby zwrócić więcej niż jeden wynik, musisz wykonać iterację po cursor lub użyj jednego z innych fetch* metody.




Używanie parametrów zapytania w SQL

W poprzedniej sekcji utworzyłeś bazę danych, nawiązałeś z nią połączenie i wykonałeś zapytanie. Użyte zapytanie było statyczne . Innymi słowy, nie miał żadnych parametrów . Teraz zaczniesz używać parametrów w swoich zapytaniach.

Najpierw zaimplementujesz funkcję, która sprawdza, czy użytkownik jest administratorem. is_admin() akceptuje nazwę użytkownika i zwraca mu status administratora:

# 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

Ta funkcja wykonuje zapytanie, aby pobrać wartość admin kolumna dla podanej nazwy użytkownika. Użyłeś fetchone() aby zwrócić krotkę z pojedynczym wynikiem. Następnie rozpakowałeś tę krotkę do zmiennej admin . Aby przetestować swoją funkcję, sprawdź nazwy użytkowników:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True

Jak na razie dobrze. Funkcja zwróciła oczekiwany wynik dla obu użytkowników. Ale co z nieistniejącym użytkownikiem? Spójrz na ten ślad zwrotny Pythona:

>>>
>>> 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

Gdy użytkownik nie istnieje, TypeError jest podniesiony. Dzieje się tak, ponieważ .fetchone() zwraca None gdy nie zostaną znalezione żadne wyniki i rozpakowanie None podnosi TypeError . Jedyne miejsce, w którym możesz rozpakować krotkę, to miejsce, w którym wpisujesz admin z result .

Aby obsłużyć nieistniejących użytkowników, utwórz specjalny przypadek, gdy result to 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

Tutaj dodałeś specjalny przypadek obsługi None . Jeśli username nie istnieje, to funkcja powinna zwrócić False . Jeszcze raz przetestuj funkcję na niektórych użytkownikach:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False

Świetny! Funkcja może teraz obsługiwać również nieistniejące nazwy użytkowników.



Wykorzystywanie parametrów zapytania za pomocą wstrzykiwania języka Python SQL

W poprzednim przykładzie użyto interpolacji ciągów do wygenerowania zapytania. Następnie wykonałeś zapytanie i wysłałeś wynikowy ciąg bezpośrednio do bazy danych. Jest jednak coś, co mogłeś przeoczyć podczas tego procesu.

Wróć myślami do username argument przekazany do is_admin() . Co dokładnie reprezentuje ta zmienna? Możesz założyć, że username to po prostu ciąg, który reprezentuje rzeczywistą nazwę użytkownika. Jednak, jak zaraz zobaczysz, intruz może łatwo wykorzystać tego rodzaju niedopatrzenie i wyrządzić poważne szkody, wykonując wstrzykiwanie SQL SQL w języku Python.

Spróbuj sprawdzić, czy następujący użytkownik jest administratorem, czy nie:

>>>
>>> is_admin("'; select true; --")
True

Czekaj… Co się właśnie stało?

Przyjrzyjmy się jeszcze raz realizacji. Wydrukuj aktualnie wykonywane zapytanie w bazie danych:

>>>
>>> print("select admin from users where username = '%s'" % "'; select true; --")
select admin from users where username = ''; select true; --'

Otrzymany tekst zawiera trzy stwierdzenia. Aby dokładnie zrozumieć, jak działa Python SQL injection, musisz sprawdzić każdą część z osobna. Pierwsze stwierdzenie brzmi następująco:

select admin from users where username = '';

To jest Twoje zamierzone zapytanie. Średnik (; ) kończy zapytanie, więc wynik tego zapytania nie ma znaczenia. Dalej jest drugie stwierdzenie:

select true;

To oświadczenie zostało skonstruowane przez intruza. Został zaprojektowany tak, aby zawsze zwracał True .

Na koniec widzisz ten krótki fragment kodu:

--'

Ten fragment kodu rozbraja wszystko, co nastąpi po nim. Intruz dodał symbol komentarza (-- ), aby zamienić w komentarz wszystko, co mogłeś umieścić po ostatnim symbolu zastępczym.

Kiedy wykonasz funkcję z tym argumentem, zawsze zwróci True . Jeśli na przykład użyjesz tej funkcji na swojej stronie logowania, intruz może zalogować się przy użyciu nazwy użytkownika '; select true; -- i otrzymają dostęp.

Jeśli uważasz, że to źle, może być jeszcze gorzej! Intruzi znający strukturę tabeli mogą użyć Python SQL injection do spowodowania trwałych uszkodzeń. Na przykład intruz może wstrzyknąć instrukcję aktualizacji, aby zmienić informacje w bazie danych:

>>>
>>> is_admin('haki')
False
>>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --")
True
>>> is_admin('haki')
True

Zróbmy to jeszcze raz:

';

Ten fragment kodu kończy zapytanie, tak jak w poprzednim wstrzyknięciu. Następne stwierdzenie brzmi następująco:

update users set admin = 'true' where username = 'haki';

Ta sekcja aktualizuje admin na true dla użytkownika haki .

Na koniec jest ten fragment kodu:

select true; --

Podobnie jak w poprzednim przykładzie, ten fragment zwraca true i komentuje wszystko, co następuje po nim.

Dlaczego jest gorzej? Cóż, jeśli intruzowi uda się wykonać funkcję za pomocą tych danych wejściowych, to użytkownik haki zostanie administratorem:

psycopgtest=# select * from users;
 username | admin
----------+-------
 ran      | t
 haki     | t
(2 rows)

Intruz nie musi już korzystać z hacka. Mogą po prostu zalogować się przy użyciu nazwy użytkownika haki . (Jeśli intruz naprawdę chcieli wyrządzić krzywdę, mogliby nawet wydać DROP DATABASE polecenie.)

Zanim zapomnisz, przywróć haki powrót do pierwotnego stanu:

psycopgtest=# update users set admin = false where username = 'haki';
UPDATE 1

Dlaczego tak się dzieje? Cóż, co wiesz o username argument? Wiesz, że powinien to być ciąg znaków reprezentujący nazwę użytkownika, ale tak naprawdę nie sprawdzasz ani nie wymuszasz tego potwierdzenia. To może być niebezpieczne! Właśnie tego szukają napastnicy, gdy próbują włamać się do Twojego systemu.


Tworzenie parametrów bezpiecznego zapytania

W poprzedniej sekcji widziałeś, jak intruz może wykorzystać twój system i uzyskać uprawnienia administratora, używając starannie spreparowanego ciągu. Problem polegał na tym, że zezwolono na wykonanie wartości przekazanej od klienta bezpośrednio do bazy danych, bez wykonywania jakiejkolwiek kontroli lub walidacji. Wstrzyknięcia SQL opierają się na tego typu podatności.

Za każdym razem, gdy dane wprowadzone przez użytkownika są używane w zapytaniu do bazy danych, istnieje możliwość podatności na wstrzyknięcie SQL. Kluczem do zapobiegania wstrzykiwaniu Pythona SQL jest upewnienie się, że wartość jest używana zgodnie z zamierzeniami programisty. W poprzednim przykładzie zamierzałeś użyć username do wykorzystania jako ciąg. W rzeczywistości był używany jako surowa instrukcja SQL.

Aby upewnić się, że wartości są używane zgodnie z przeznaczeniem, musisz uciec wartość. Na przykład, aby uniemożliwić intruzom wstrzykiwanie surowego kodu SQL w miejsce argumentu ciągu, możesz zmienić znaki cudzysłowu:

>>>
>>> # BAD EXAMPLE. DON'T DO THIS!
>>> username = username.replace("'", "''")

To tylko przykład. Istnieje wiele znaków specjalnych i scenariuszy, o których należy pomyśleć, próbując zapobiec wstrzykiwaniu języka Python SQL. Na szczęście dla Ciebie, nowoczesne adaptery baz danych mają wbudowane narzędzia do zapobiegania wstrzykiwaniu SQL SQL za pomocą parametrów zapytania . Są one używane zamiast interpolacji zwykłych ciągów do tworzenia zapytania z parametrami.

Uwaga: Różne adaptery, bazy danych i języki programowania odwołują się do parametrów zapytania pod różnymi nazwami. Popularne nazwy obejmują zmienne wiązania , zmienne zastępcze i zmienne podstawienia .

Teraz, gdy lepiej rozumiesz lukę w zabezpieczeniach, możesz przepisać funkcję, używając parametrów zapytania zamiast interpolacji ciągów:

 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

Oto, co różni się w tym przykładzie:

  • W wierszu 9, użyłeś nazwanego parametru username aby wskazać, gdzie powinna iść nazwa użytkownika. Zwróć uwagę, jak parametr username nie jest już otoczony pojedynczymi cudzysłowami.

  • W wierszu 11, przekazałeś wartość username jako drugi argument cursor.execute() . Połączenie użyje typu i wartości username podczas wykonywania zapytania w bazie danych.

Aby przetestować tę funkcję, wypróbuj kilka prawidłowych i nieprawidłowych wartości, w tym niebezpiecznego ciągu sprzed:

>>>
>>> is_admin('haki')
False
>>> is_admin('ran')
True
>>> is_admin('foo')
False
>>> is_admin("'; select true; --")
False

Zdumiewający! Funkcja zwróciła oczekiwany wynik dla wszystkich wartości. Co więcej, niebezpieczny sznurek już nie działa. Aby zrozumieć dlaczego, możesz sprawdzić zapytanie wygenerowane przez 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; --'

Połączenie potraktowało wartość username jako ciąg i uciekł wszystkie znaki, które mogą zakończyć ciąg i wprowadzić wstrzykiwanie Pythona SQL.



Przekazywanie parametrów bezpiecznego zapytania

Adaptery baz danych zwykle oferują kilka sposobów przekazywania parametrów zapytania. Nazwane symbole zastępcze są zazwyczaj najlepsze pod względem czytelności, ale niektóre implementacje mogą korzystać z innych opcji.

Rzućmy okiem na kilka dobrych i złych sposobów używania parametrów zapytania. Poniższy blok kodu pokazuje typy zapytań, których należy unikać:

# 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}'");

Każde z tych oświadczeń przekazuje username z klienta bezpośrednio do bazy danych, bez wykonywania jakiejkolwiek kontroli lub walidacji. Ten rodzaj kodu jest gotowy do zaproszenia Pythona SQL injection.

W przeciwieństwie do tego, tego typu zapytania powinny być bezpieczne do wykonania:

# 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});

W tych instrukcjach username jest przekazywany jako nazwany parametr. Teraz baza danych użyje określonego typu i wartości username podczas wykonywania zapytania, oferując ochronę przed wstrzyknięciem Pythona SQL.




Korzystanie z kompozycji SQL

Do tej pory używałeś parametrów dla literałów. Literały to wartości, takie jak liczby, ciągi i daty. Ale co, jeśli masz przypadek użycia, który wymaga utworzenia innego zapytania — takiego, w którym parametr jest czymś innym, na przykład nazwą tabeli lub kolumny?

Zainspirowani poprzednim przykładem, zaimplementujmy funkcję, która przyjmuje nazwę tabeli i zwraca liczbę wierszy w tej tabeli:

# 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

Spróbuj wykonać funkcję w tabeli użytkowników:

>>>
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'
                        ^

Polecenie nie wygenerowało kodu SQL. Jak już widzieliście, adapter bazy danych traktuje zmienną jako ciąg znaków lub literał. Jednak nazwa tabeli nie jest zwykłym ciągiem. Tutaj pojawia się kompozycja SQL.

Wiesz już, że używanie interpolacji ciągów do tworzenia kodu SQL nie jest bezpieczne. Na szczęście Psycopg udostępnia moduł o nazwie psycopg.sql aby pomóc Ci bezpiecznie komponować zapytania SQL. Przepiszmy funkcję za pomocą 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

W tej implementacji są dwie różnice. Najpierw użyłeś sql.SQL() skomponować zapytanie. Następnie użyłeś sql.Identifier() aby opisać wartość argumentu table_name . (identyfikator to nazwa kolumny lub tabeli).

Uwaga: Użytkownicy popularnego pakietu django-debug-toolbar może otrzymać błąd w panelu SQL dla zapytań skomponowanych za pomocą psycopg.sql.SQL() . Oczekuje się, że poprawka zostanie wydana w wersji 2.0.

Teraz spróbuj wykonać funkcję na users tabela:

>>>
>>> count_rows('users')
2

Świetny! Następnie zobaczmy, co się dzieje, gdy tabela nie istnieje:

>>>
>>> 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"
                        ^

Funkcja rzuca UndefinedTable wyjątek. W poniższych krokach użyjesz tego wyjątku jako wskazania, że ​​Twoja funkcja jest bezpieczna przed atakiem Python SQL injection.

Uwaga: Wyjątek UndefinedTable został dodany w psycopg2 w wersji 2.8. Jeśli pracujesz z wcześniejszą wersją Psycopg, otrzymasz inny wyjątek.

Aby to wszystko połączyć, dodaj opcję zliczania wierszy w tabeli do pewnego limitu. Ta funkcja może być przydatna w przypadku bardzo dużych tabel. Aby to zaimplementować, dodaj LIMIT klauzula do zapytania wraz z parametrami zapytania dotyczącymi wartości limitu:

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

W tym bloku kodu dodałeś adnotację limit za pomocą sql.Literal() . Podobnie jak w poprzednim przykładzie, psycopg powiąże wszystkie parametry zapytania jako literały przy użyciu prostego podejścia. Jednak przy użyciu sql.SQL() , musisz wyraźnie opisać każdy parametr za pomocą sql.Identifier() lub sql.Literal() .

Uwaga: Niestety specyfikacja Pythona API nie dotyczy wiązania identyfikatorów, a jedynie literały. Psycopg to jedyny popularny adapter, który dodał możliwość bezpiecznego komponowania SQL zarówno z literałami, jak i identyfikatorami. Fakt ten sprawia, że ​​jeszcze ważniejsze jest zwrócenie szczególnej uwagi podczas wiązania identyfikatorów.

Uruchom funkcję, aby upewnić się, że działa:

>>>
>>> count_rows('users', 1)
1
>>> count_rows('users', 10)
2

Teraz, gdy widzisz, że funkcja działa, upewnij się, że jest również bezpieczna:

>>>
>>> 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...
                            ^

Ten ślad pokazuje, że psycopg ominął wartość, a baza danych potraktowała ją jako nazwę tabeli. Ponieważ tabela o tej nazwie nie istnieje, UndefinedTable zgłoszono wyjątek i nie zostałeś zhakowany!



Wniosek

Udało Ci się zaimplementować funkcję, która komponuje dynamiczny SQL bez narażasz swój system na ryzyko wstrzyknięcia Pythona SQL! W zapytaniu użyłeś zarówno literałów, jak i identyfikatorów bez narażania bezpieczeństwa.

Nauczyłeś się:

  • Jakie wstrzyknięcie Pythona SQL jest i jak można to wykorzystać
  • Jak zapobiegać wstrzykiwaniu Pythona SQL za pomocą parametrów zapytania
  • Jak bezpiecznie komponować instrukcje SQL które używają literałów i identyfikatorów jako parametrów

Możesz teraz tworzyć programy, które mogą wytrzymać ataki z zewnątrz. Idź naprzód i udaremnij atak hakerom!



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Migracja z AnswerHub do WordPress:opowieść o 10 technologiach

  2. Jak tworzyć tabele bazy danych za pomocą SQL

  3. Ograniczenie elastyczności danych w bazie danych NoSQL

  4. Jak uruchamiać zadania zdalne z IRI Workbench

  5. Błędy, pułapki i najlepsze praktyki T-SQL – funkcje okien