Czy instrukcje przygotowane przez PDO są wystarczające, aby zapobiec wstrzyknięciu SQL?

661

Powiedzmy, że mam taki kod:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Dokumentacja ChNP mówi:

Parametry gotowych instrukcji nie muszą być cytowane; kierowca obsługuje to za ciebie.

Czy to naprawdę wszystko, co muszę zrobić, aby uniknąć zastrzyków SQL? Czy to naprawdę takie proste?

Możesz założyć MySQL, jeśli robi to różnicę. Jestem naprawdę ciekawy tylko użycia przygotowanych instrukcji przeciwko iniekcji SQL. W tym kontekście nie dbam o XSS ani inne możliwe luki.

Mark Biek
źródło
5
lepsze podejście 7. numer odpowiedzi stackoverflow.com/questions/134099/...
NullPoiиteя

Odpowiedzi:

807

Krótka odpowiedź brzmi NIE , przygotowania PDO nie ochronią cię przed wszystkimi możliwymi atakami SQL-Injection. W przypadku niektórych niejasnych przypadków na krawędziach.

Dostosowuję tę odpowiedź, aby mówić o PDO ...

Długa odpowiedź nie jest taka łatwa. Opiera się na pokazanym tutaj ataku .

Atak

Zacznijmy więc od pokazania ataku ...

$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));

W pewnych okolicznościach zwróci więcej niż 1 wiersz. Przeanalizujmy, co się tutaj dzieje:

  1. Wybieranie zestawu znaków

    $pdo->query('SET NAMES gbk');

    Aby ten atak zadziałał, potrzebujemy kodowania, którego serwer oczekuje w połączeniu zarówno do kodowania 'jak w ASCII tj. 0x27 I do posiadania znaku, którego ostatnim bajtem jest ASCII \tj 0x5c. Jak się okazuje, istnieje 5 takich kodowania obsługiwane w MySQL 5.6 domyślnie: big5, cp932, gb2312, gbki sjis. gbkTutaj wybierzemy .

    Teraz bardzo ważne jest, aby zauważyć użycie SET NAMEStutaj. To ustawia zestaw znaków NA SERWERZE . Jest na to inny sposób, ale niedługo się tam dostaniemy.

  2. Ładunek

    Ładunek, którego użyjemy do tego wstrzyknięcia, zaczyna się od sekwencji bajtów 0xbf27. W gbkto niepoprawny znak wielobajtowy; w latin1to jest ciąg ¿'. Zauważ, że w latin1 i gbk , 0x27na własną rękę jest dosłowny 'charakter.

    Wybraliśmy ten ładunek, ponieważ jeśli addslashes()go wywołamy, wstawimy ASCII, \tj. 0x5cPrzed 'znakiem. Więc chcemy skończyć z 0xbf5c27, która gbkjest sekwencją dwóch znaków: 0xbf5cnastępuje 0x27. Lub innymi słowy, prawidłowy znak, po którym następuje nieskalowany '. Ale my nie używamy addslashes(). Przejdźmy do następnego kroku ...

  3. $ stmt-> execute ()

    Ważne jest, aby zdać sobie sprawę z tego, że PDO domyślnie NIE wykonuje prawdziwie przygotowanych instrukcji. Emuluje je (dla MySQL). Dlatego PDO wewnętrznie buduje ciąg zapytania, wywołując mysql_real_escape_string()(funkcja MySQL C API) na każdej wartości powiązanego ciągu.

    Wywołanie interfejsu API C mysql_real_escape_string()różni się addslashes()tym, że zna zestaw znaków połączenia. Dzięki temu może poprawnie wykonać zmianę znaczenia dla zestawu znaków, którego oczekuje serwer. Jednak do tego momentu klient uważa, że ​​nadal używamy latin1do połączenia, ponieważ nigdy nie powiedzieliśmy tego inaczej. Poinformowaliśmy serwer , którego używamy gbk, ale klient nadal uważa, że ​​tak latin1.

    Dlatego wezwanie do mysql_real_escape_string()wstawienia odwrotnego ukośnika, a my mamy wolną 'postać w naszej „ucieczce” treści! W rzeczywistości, jeśli byliśmy patrzeć $varw gbkzestawie znaków, to widzimy:

    OR „LUB 1 = 1 / *

    Właśnie tego wymaga atak.

  4. Zapytanie

    Ta część jest tylko formalnością, ale oto renderowane zapytanie:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

Gratulacje, właśnie pomyślnie zaatakowałeś program przy użyciu przygotowanych instrukcji PDO ...

Prosta poprawka

Teraz warto zauważyć, że można temu zapobiec, wyłączając emulowane przygotowane instrukcje:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Będzie to zwykle prowadzi do prawdziwej przygotowanego zestawienia (czyli danych wysyłanych przez w oddzielnym pakiecie z zapytania). Należy jednak pamiętać, że PDO będzie cicho awaryjna do emulacji oświadczenia, że MySQL nie może przygotować natywnie: ci, że może są wymienione w instrukcji, ale uważaj, aby wybrać odpowiednią wersję serwera).

Prawidłowa poprawka

Problem polega na tym, że nie nazywają C API mysql_set_charset()zamiast SET NAMES. Gdyby tak było, byłoby nam dobrze, pod warunkiem, że korzystamy z wersji MySQL od 2006 roku.

Jeśli używasz wcześniejszej wersji MySQL, potem błąd w mysql_real_escape_string()oznaczało, że nieprawidłowe znaki wielobajtowe, takie jak te w naszym ładowności były traktowane jako pojedyncze bajty dla celów ucieczce nawet jeśli klient został prawidłowo poinformowany o kodowaniu połączeń i tak ten atak wciąż się udaje. Błąd został naprawiony w MySQL 4.1.20 , 5.0.22 i 5.1.11 .

Ale najgorsze jest to, że PDOnie ujawniało C API mysql_set_charset()do 5.3.6, więc we wcześniejszych wersjach nie mogło zapobiec temu atakowi dla każdego możliwego polecenia! Jest teraz widoczny jako parametr DSN , którego należy użyć zamiast SET NAMES ...

The Saving Grace

Jak powiedzieliśmy na wstępie, aby ten atak zadziałał, połączenie z bazą danych musi zostać zakodowane przy użyciu podatnego zestawu znaków. nieutf8mb4 jest podatny na zagrożenia, a jednak może obsługiwać każdą postać Unicode: możesz więc zamiast tego użyć tej opcji - ale jest ona dostępna tylko od MySQL 5.5.3. Alternatywą jest utf8, która również nie jest wrażliwa i może obsługiwać cały podstawowy wielojęzyczny samolot Unicode .

Alternatywnie możesz włączyć NO_BACKSLASH_ESCAPEStryb SQL, który (między innymi) zmienia działanie mysql_real_escape_string(). Po włączeniu tego trybu 0x27zostanie zastąpione 0x2727zamiast zamiast, 0x5c27dlatego proces zmiany znaczenia nie może utworzyć prawidłowych znaków w żadnym z wrażliwych kodowań, w których wcześniej nie istniały (tj. 0xbf27Nadal jest 0xbf27itp.) - więc serwer nadal odrzuca ciąg jako nieprawidłowy . Jednak zobacz odpowiedź @ eggyal na inną podatność, która może wyniknąć z używania tego trybu SQL (choć nie z PDO).

Bezpieczne przykłady

Następujące przykłady są bezpieczne:

mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Ponieważ serwer oczekuje utf8...

mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");

Ponieważ właściwie ustawiliśmy zestaw znaków, aby klient i serwer pasowały do ​​siebie.

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES gbk');
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Ponieważ wyłączyliśmy emulowane przygotowane oświadczenia.

$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));

Ponieważ poprawnie ustawiliśmy zestaw znaków.

$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();

Ponieważ MySQLi cały czas wykonuje prawdziwe przygotowane instrukcje.

Podsumowanie

Jeśli ty:

  • Używaj nowoczesnych wersji MySQL (późny 5.1, wszystkie 5.5, 5.6 itd.) ORAZ parametru zestawu znaków DSN PDO (w PHP ≥ 5.3.6)

LUB

  • Nie używaj wrażliwego zestawu znaków do kodowania połączenia (używasz tylko utf8/ latin1/ ascii/ etc)

LUB

  • Włącz NO_BACKSLASH_ESCAPEStryb SQL

Jesteś w 100% bezpieczny.

W przeciwnym razie jesteś podatny na zagrożenia, nawet jeśli używasz przygotowanych instrukcji PDO ...

Uzupełnienie

Powoli pracuję nad łatką, aby zmienić domyślną opcję, aby nie emulować przygotowań do przyszłej wersji PHP. Problem, na który wpadam, polega na tym, że wiele testów psuje się, kiedy to robię. Jednym z problemów jest to, że emulowane przygotowania przygotują tylko błędy składniowe podczas wykonywania, ale prawdziwe przygotowania przygotują błędy podczas przygotowania. Może to powodować problemy (i jest jednym z powodów, dla których testy się psują).

ircmaxell
źródło
47
To najlepsza odpowiedź, jaką znalazłem ... czy możesz podać link, aby uzyskać więcej informacji?
StaticVariable 30.09.12
1
@nicogawenda: to był inny błąd. Przed wersją 5.0.22 mysql_real_escape_stringnie obsługiwał poprawnie przypadków, w których połączenie było poprawnie ustawione na BIG5 / GBK. Tak więc nawet wywołanie mysql_set_charset()mysql <5.0.22 byłoby podatne na ten błąd! Więc nie, ten post ma nadal zastosowanie do 5.0.22 (ponieważ mysql_real_escape_string jest tylko charset od połączeń, z mysql_set_charset()których ten post mówi o pomijaniu) ...
ircmaxell
1
@progfa Niezależnie od tego, czy tak, czy nie, zawsze powinieneś sprawdzić poprawność danych wejściowych na serwerze, zanim zrobisz cokolwiek z danymi użytkownika.
Tek
2
Należy pamiętać, że NO_BACKSLASH_ESCAPESmożna również wprowadzić nowe luki w zabezpieczeniach: stackoverflow.com/a/23277864/1014813
lepix
2
@slevin „OR 1 = 1” to symbol zastępczy na cokolwiek chcesz. Tak, szuka wartości w nazwie, ale wyobraź sobie, że część „OR 1 = 1” brzmiała „WYBÓR UNII * OD użytkowników”. Teraz kontrolujesz zapytanie i jako takie może nadużywać go ...
ircmaxell
515

Przygotowane instrukcje / sparametryzowane zapytania są zasadniczo wystarczające, aby zapobiec wstrzyknięciu pierwszego rzędu do tej instrukcji * . Jeśli użyjesz niezaznaczonej dynamicznej sql gdziekolwiek indziej w twojej aplikacji, nadal będziesz podatny na zastrzyk drugiego rzędu .

Wstrzykiwanie drugiego rzędu oznacza, że ​​dane zostały cyklicznie przeładowane przez bazę danych, zanim zostały uwzględnione w zapytaniu, i jest znacznie trudniejsze do pobrania. AFAIK, prawie nigdy nie widzisz prawdziwych ataków inżynieryjnych drugiego rzędu, ponieważ atakujący zwykle łatwiej im się sprzyja inżynierii społecznej, ale czasami pojawiają się błędy drugiego rzędu z powodu dodatkowych łagodnych 'postaci lub podobnych.

Możesz wykonać atak wstrzykiwania drugiego rzędu, gdy możesz spowodować zapisanie wartości w bazie danych, która później zostanie użyta jako literał w zapytaniu. Jako przykład załóżmy, że podczas tworzenia konta w witrynie internetowej wprowadzasz następujące informacje jako nową nazwę użytkownika (przy założeniu bazy danych MySQL dla tego pytania):

' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '

Jeśli nie ma żadnych innych ograniczeń dotyczących nazwy użytkownika, przygotowana instrukcja nadal upewniałaby się, że powyższe osadzone zapytanie nie wykona się w momencie wstawiania i zapisuje wartość poprawnie w bazie danych. Wyobraź sobie jednak, że później aplikacja pobiera nazwę użytkownika z bazy danych i używa konkatenacji ciągów, aby uwzględnić tę wartość w nowym zapytaniu. Możesz zobaczyć hasło kogoś innego. Ponieważ kilka pierwszych nazwisk w tabeli użytkowników to zazwyczaj administratorzy, być może właśnie rozdałeś farmę. (Uwaga: to kolejny powód, dla którego nie należy przechowywać haseł w postaci zwykłego tekstu!)

Widzimy więc, że przygotowane oświadczenia są wystarczające dla pojedynczego zapytania, ale sami są nie wystarczające do ochrony przed sql atakami wstrzyknięcia całej całej aplikacji, ze względu na brak mechanizmu egzekwowania wszystkie dostępu do bazy danych w aplikacji wykorzystuje bezpieczne kod. Jednak używane w ramach dobrego projektowania aplikacji - które mogą obejmować takie praktyki, jak przegląd kodu lub analiza statyczna, lub użycie ORM, warstwy danych lub warstwy usługi, która ogranicza dynamiczne tworzenie kodu SQL - przygotowane instrukcjepodstawowym narzędziem do rozwiązania Sql Injection problem.Jeśli przestrzegasz dobrych zasad projektowania aplikacji, takich jak dostęp do danych jest oddzielony od reszty programu, łatwo jest wymusić lub skontrolować, czy każde zapytanie poprawnie używa parametryzacji. W takim przypadku iniekcja sql (zarówno pierwszego, jak i drugiego rzędu) jest całkowicie uniemożliwiona.


* Okazuje się, że MySql / PHP są (w porządku, były) po prostu głupie w kwestii obsługi parametrów, gdy w grę wchodzą szerokie znaki, i wciąż jest rzadki przypadek opisany w innej wysoko głosowanej odpowiedzi tutaj, która może pozwolić na wstrzyknięcie przez sparametryzowane pytanie.

Joel Coehoorn
źródło
6
To interesujące. Nie byłem świadomy pierwszego rzędu vs. drugiego rzędu. Czy możesz bardziej szczegółowo wyjaśnić, jak działa 2. zamówienie?
Mark Biek
193
Jeśli WSZYSTKIE twoje zapytania są sparametryzowane, jesteś również chroniony przed iniekcją drugiego rzędu. Wstrzyknięcie pierwszego rzędu zapomina, że ​​dane użytkownika są niewiarygodne. Zastrzyk drugiego rzędu zapomina, że ​​dane w bazie danych są niewiarygodne (ponieważ pierwotnie pochodziły od użytkownika).
cjm
6
Dzięki cjm. Uważam również, że ten artykuł jest pomocny w wyjaśnieniu zastrzyków drugiego rzędu: codeproject.com/KB/database/SqlInjectionAttacks.aspx
Mark Biek
49
O tak. Ale co z zastrzykiem trzeciego rzędu . Muszę być tego świadomy.
troelskn
81
@troelskn to musi być miejsce, w którym deweloper jest źródłem niewiarygodnych danych
MikeMurko,
45

Nie, nie zawsze są.

Zależy to od tego, czy zezwalasz na umieszczanie danych wejściowych użytkownika w samym zapytaniu. Na przykład:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

byłby podatny na zastrzyki SQL i użycie przygotowanych instrukcji w tym przykładzie nie zadziała, ponieważ dane wejściowe użytkownika są używane jako identyfikator, a nie dane. Właściwą odpowiedzią byłoby tutaj zastosowanie filtrowania / sprawdzania poprawności, takich jak:

$dbh = new PDO("blahblah");

$tableToUse = $_GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))    
 $tableToUse = 'users';

$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

Uwaga: nie można używać PDO do wiązania danych wykraczających poza DDL (język definicji danych), tzn. To nie działa:

$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');

Powodem, dla którego powyżej nie działa, bo jest DESCi ASCnie są dane . PDO może uciec tylko dla danych . Po drugie, nie można nawet umieszczać 'wokół niego cudzysłowów. Jedynym sposobem, aby umożliwić łatwy sortowania jest wybrany filtr ręcznie i sprawdzić, że jest to albo DESCalbo ASC.

Wieża
źródło
11
Czy coś mi tu brakuje, ale nie chodzi o sedno przygotowanych instrukcji, aby uniknąć traktowania sql jak łańcucha? Czy nie byłoby czegoś takiego jak $ dbh-> przygotuj ('SELECT * FROM: tableToUse where username =: nazwa użytkownika'); obejść swój problem?
Rob Forrest,
4
@RobForrest tak, brakuje ci :). Powiązane dane działają tylko w języku DDL (Data Definition Language). Potrzebujesz tych cytatów i właściwego ucieczki. Umieszczenie cudzysłowów w innych częściach zapytania powoduje jego duże prawdopodobieństwo. Na przykład SELECT * FROM 'table'może być niewłaściwy, jak powinien, SELECT * FROM `table`lub bez żadnych backsticksów. Potem kilka rzeczy, jak ORDER BY DESC, gdzie DESCpochodzi od użytkownika nie może być po prostu uciekł. Zatem praktyczne scenariusze są raczej nieograniczone.
Tower
8
Zastanawiam się, w jaki sposób 6 osób mogło wypowiedzieć się na temat komentarza, proponując wyraźnie niewłaściwe użycie przygotowanego oświadczenia. Gdyby choć raz tego spróbowali, od razu odkryliby, że użycie nazwanego parametru zamiast nazwy tabeli nie zadziała.
Félix Gagnon-Grenier
Oto świetny samouczek na temat PDO, jeśli chcesz się go nauczyć. a2znotes.blogspot.in/2014/09/introduction-to-pdo.html
RN Kushwaha
11
Nigdy nie należy używać ciągu zapytania / treści testu POST, aby wybrać tabelę do użycia. Jeśli nie masz modeli, użyj przynajmniej a, switchaby uzyskać nazwę tabeli.
ZiggyTheHamster
29

Tak, wystarczy. Sposób działania ataków typu iniekcyjnego polega na tym, że interpreter (baza danych) w jakiś sposób ocenia coś, co powinno być danymi, jakby to był kod. Jest to możliwe tylko wtedy, gdy łączysz kod i dane na tym samym nośniku (np. Gdy konstruujesz zapytanie jako ciąg znaków).

Zapytania sparametryzowane działają, wysyłając kod i dane osobno, więc nigdy nie byłoby w tym dziury.

Nadal jednak możesz być podatny na ataki typu iniekcyjnego. Na przykład, jeśli użyjesz danych na stronie HTML, możesz podlegać atakom typu XSS.

troelskn
źródło
10
„Nigdy” jest sposobem, zawyżając go do punktu jest mylące. Jeśli niepoprawnie używasz przygotowanych instrukcji, nie jest to o wiele lepsze niż ich nie używanie. (Oczywiście „przygotowane oświadczenie”, do którego wprowadzono dane użytkownika, nie spełnia celu ... ale faktycznie widziałem, jak to zrobiono. I przygotowane oświadczenia nie mogą obsługiwać identyfikatorów (nazw tabel itp.) Jako parametrów.) Dodaj w tym celu niektóre sterowniki PDO emulują przygotowane instrukcje i istnieje możliwość, aby zrobiły to niepoprawnie (na przykład przez półprawidłowe parsowanie SQL). Krótka wersja: nigdy nie zakładaj, że to takie proste.
cHao
29

Nie, to nie wystarczy (w niektórych szczególnych przypadkach)! Domyślnie PDO używa emulowanych przygotowanych instrukcji, gdy używasz MySQL jako sterownika bazy danych. Zawsze powinieneś wyłączyć emulowane przygotowane instrukcje podczas korzystania z MySQL i PDO:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Kolejną rzeczą, którą należy zawsze zrobić, jest ustawienie prawidłowego kodowania bazy danych:

$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');

Zobacz także podobne pytanie: Jak mogę zapobiec wstrzykiwaniu SQL w PHP?

Zauważ również, że dotyczy to tylko strony bazy danych rzeczy, które nadal będziesz musiał oglądać podczas wyświetlania danych. Np. Używając htmlspecialchars()ponownie z poprawnym stylem kodowania i cytowania.

PeeHaa
źródło
14

Osobiście zawsze najpierw uruchamiałbym jakieś dane sanitarne na danych, ponieważ nigdy nie można ufać danych wejściowych użytkownika, jednak podczas używania symboli zastępczych / wiązania parametrów wprowadzone dane są wysyłane do serwera osobno do instrukcji sql, a następnie łączone razem. Kluczem tutaj jest to, że wiąże to dostarczone dane z określonym typem i konkretnym zastosowaniem i eliminuje możliwość zmiany logiki instrukcji SQL.

JimmyJ
źródło
1

Eaven, jeśli masz zamiar zapobiec front-endowi wstrzykiwania sql, używając czeków HTML lub JS, musisz wziąć pod uwagę, że kontrole frontonu są „pomijalne”.

Możesz wyłączyć js lub edytować wzór za pomocą narzędzia programistycznego front-end (obecnie wbudowanego w Firefoxa lub Chrome).

Tak więc, aby zapobiec wstrzyknięciu SQL, dobrze byłoby zdezynfekować backend daty wejściowej w twoim kontrolerze.

Chciałbym zasugerować, abyś użył natywnej funkcji PHP filter_input () w celu oczyszczenia wartości GET i INPUT.

Jeśli chcesz korzystać z bezpieczeństwa, w przypadku sensownych zapytań do bazy danych, chciałbym zasugerować użycie wyrażenia regularnego do sprawdzania poprawności formatu danych. preg_match () pomoże ci w tym przypadku! Ale uważaj! Silnik Regex nie jest tak lekki. Używaj go tylko w razie potrzeby, w przeciwnym razie wydajność aplikacji spadnie.

Bezpieczeństwo ma swoje koszty, ale nie marnuj swojej wydajności!

Prosty przykład:

jeśli chcesz dokładnie sprawdzić, czy wartość otrzymana z GET jest liczbą, mniejszą niż 99, jeśli (! preg_match ('/ [0-9] {1,2} /')) {...} jest większy od

if (isset($value) && intval($value)) <99) {...}

Zatem ostateczna odpowiedź brzmi: „Nie! Przygotowane oświadczenia PDO nie zapobiegają wszelkiego rodzaju iniekcjom SQL”; Nie zapobiega nieoczekiwanym wartościom, tylko nieoczekiwanej konkatenacji

snipershady
źródło
5
Twój wspólny zmysł