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 lublocalhost
. -
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żytkownikapostgres
. -
password
to hasło dla osoby, którą określiłeś wuser
. 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 parametrusername
nie jest już otoczony pojedynczymi cudzysłowami. -
W wierszu 11, przekazałeś wartość
username
jako drugi argumentcursor.execute()
. Połączenie użyje typu i wartościusername
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!