Czy deklaracje PDO prepared są wystarczające, aby zapobiec SQL injection?

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 PDO mówi:

Parametry do przygotowanych instrukcji nie muszą być cytowane; sterownik obsługuje je 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 to robi różnicę. Poza tym ciekawi mnie tylko użycie gotowych wyrażeń przeciwko SQL injection. W tym kontekście, nie obchodzi mnie XSS lub inne możliwe luki.

Author: Patrick Hofman, 2008-09-25

7 answers

Krótka odpowiedź to Nie , PDO nie obroni Cię przed wszystkimi możliwymi atakami SQL-Injection. Dla niektórych niejasnych przypadków krawędzi.

Dostosowuję tę odpowiedź do rozmowy o PDO...

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

Atak

Zacznijmy 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. Zbadajmy co tu się dzieje:

  1. Wybór zestawu znaków

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

    Aby ten atak zadziałał, potrzebujemy kodowania, którego serwer oczekuje na połączeniu, aby zakodować ' jak w ASCII tzn. 0x27 i mieć jakiś znak, którego końcowy bajt to ASCII \ tzn. 0x5c. Jak się okazuje, w MySQL 5.6 domyślnie wspieranych jest 5 takich kodowań: big5, cp932, gb2312, gbk i sjis. Wybieramy gbk proszę.

    Teraz, to bardzo ważne, aby zwrócić uwagę na użycie SET NAMES tutaj. Ustawia to zestaw znaków na serwerze . Jest inny sposób, ale wkrótce tam dotrzemy.

  2. Ładowność

    Ładunek, którego użyjemy do tego zastrzyku, zaczyna się od sekwencji bajtów 0xbf27. W gbk jest to nieprawidłowy znak wielobajtowy; w latin1 jest to ciąg znaków ¿'. Zauważ, że w latin1 oraz gbk, 0x27 on its own jest literalnym znakiem '.

    Wybraliśmy ten ładunek, ponieważ gdybyśmy wywołali {[29] } na nim, wstawilibyśmy znak ASCII \, tj. 0x5c, przed znakiem '. Tak więc kończymy 0xbf5c27, który w gbk jest ciągiem dwuznakowym: 0xbf5c, po którym następuje 0x27. Lub innymi słowy, znak valid , po którym następuje unescaped '. Ale nie używamy addslashes(). Więc przejdź do następnego kroku...

  3. $stmt- > execute()

    Ważne należy zdać sobie sprawę, że PDO domyślnie wykonuje , a nie prawdziwe przygotowane Oświadczenia. Emuluje je (dla MySQL). Dlatego PDO wewnętrznie buduje łańcuch zapytania, wywołując mysql_real_escape_string() (funkcję API MySQL C) na każdej powiązanej wartości łańcucha.

    Wywołanie C API do mysql_real_escape_string() różni się od addslashes() tym, że zna zestaw znaków połączenia. W ten sposób może poprawnie wykonać ucieczkę dla zestawu znaków, którego oczekuje serwer. Jednak do tego momentu klient uważa, że jesteśmy nadal używam latin1 do połączenia, ponieważ nigdy nie mówiliśmy inaczej. Powiedzieliśmy serwerowi , że używamy gbk, ale klient nadal myśli, że to latin1.

    Dlatego wywołanie mysql_real_escape_string() wstawia ukośnik wsteczny, a my mamy znak free hanging ' w naszej "uciekającej" treści! W rzeczywistości, gdybyśmy spojrzeli na $var W zestawie znaków gbk, zobaczylibyśmy:

    縗' OR 1=1 /*
    Dokładnie tego wymaga atak.
  4. Na Zapytanie

    Ta część to tylko formalność, ale oto renderowane zapytanie:]}
    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

Gratulacje, właśnie z powodzeniem zaatakowałeś program używając deklaracji przygotowanych przez PDO...

The Simple Fix

Warto zauważyć, że można temu zapobiec, wyłączając emulowane, przygotowane instrukcje:]}
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

Spowoduje to Zwykle w wyniku prawdziwego, przygotowanego Oświadczenia (tzn. dane są przesyłane w oddzielnym pakiecie z zapytanie). Należy jednak pamiętać, że PDO będzie po cichu fallback do emulacji instrukcji, których MySQL nie może przygotować natywnie: te, które może być wymienione w podręczniku, ale uważaj, aby wybrać odpowiednią wersję serwera).

The Correct Fix

Problem polega na tym, że nie wywołaliśmy API C mysql_set_charset() zamiast SET NAMES. Jeśli tak, to byłoby dobrze, pod warunkiem, że używamy Wydania MySQL od 2006 roku.

Jeśli używasz wcześniejszego wydania MySQL, to błąd w mysql_real_escape_string() oznaczał, że niepoprawne znaki wielobajtowe, takie jak te w naszym ładunku, były traktowane jako pojedyncze bajty dla celów ucieczki , nawet jeśli klient został prawidłowo poinformowany o kodowaniu połączenia, a więc atak nadal się powiedzie. Błąd został naprawiony w MySQL 4.1.20, 5.0.22 oraz 5.1.11.

Ale najgorsze jest to, że PDO nie ujawnił API C dla mysql_set_charset() aż do 5.3.6, więc w poprzednich wersjach nie można zapobiec temu atakowi dla każdego możliwego rozkazu! Jest teraz wyświetlany jako parametr DSN , który powinien być użyty zamiast SET NAMES...

The Saving Grace

Jak powiedzieliśmy na początku, aby ten atak zadziałał, połączenie z bazą danych musi być zakodowane przy użyciu zestawu znaków podatnych na ataki. utf8mb4 jest nie podatny , a mimo to może obsługiwać każdy znak Unicode: więc możesz go użyć zamiast tego-ale ma dostępne tylko od MySQL 5.5.3. Alternatywą jest utf8, który jest również nie podatny i może obsługiwać cały Unicode Podstawowa płaszczyzna Wielojęzyczna.

Alternatywnie, możesz włączyć NO_BACKSLASH_ESCAPES tryb SQL, który (między innymi) zmienia działanie mysql_real_escape_string(). Gdy ten tryb jest włączony, 0x27 zostanie zastąpiony 0x2727 zamiast 0x5c27, a zatem proces wycinający nie może utworzyć poprawnych znaków w dowolnym z podatnych kodowań, w których wcześniej nie istniały (np. 0xbf27 jest nadal 0xbf27 itd.)- więc serwer nadal odrzuci łańcuch jako nieprawidłowy. Jednak zobacz @eggyal ' s answer dla innej luki, która może wynikać z używania tego trybu SQL (choć nie z PDO).

Bezpieczne Przykłady

Poniższe 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ż odpowiednio ustawiliśmy zestaw znaków, więc dopasowanie klienta i serwera.

$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 gotowe deklaracje.

$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ż ustawiliśmy odpowiednio 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 robi prawdziwe przygotowane wypowiedzi.

Owijanie

Jeśli:

    W przeciwieństwie do innych języków, MySQL może być używany w wielu językach (np. w języku angielskim).]}

Lub

  • Nie używaj podatnego zestawu znaków do kodowania połączenia (używasz tylkoutf8 / latin1 / ascii / etc)

Lub

  • Włącz NO_BACKSLASH_ESCAPES tryb SQL
Jesteś w 100% bezpieczny.

W przeciwnym razie, jesteś podatny nawet jeśli używasz PDO Prepared Statements...

Dodatek

Powoli pracuję nad łatką, aby zmienić domyślne ustawienia, aby nie emulować przyszłej wersji PHP. Problem, że jestem wpadam na to, że wiele testów przerywa, gdy to robię. Jednym z problemów jest to, że emulowane prepares będą rzucać tylko błędy składni na execute, ale true prepares będzie rzucać błędy na prepare. Tak, że może powodować problemy (i jest częścią powodu testy borking).

 702
Author: ircmaxell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:48

Prepared statements / parameterized queries are generally enough to prevent 1st order injection on that statement*. Jeśli używasz nie zaznaczonego dynamicznego sql gdziekolwiek indziej w swojej aplikacji, nadal jesteś podatny na 2nd order injection.

Drugi zastrzyk zamówienia oznacza, że dane zostały raz przepuszczone przez bazę danych, zanim zostały włączone do zapytania i jest znacznie trudniejsze do wykonania. AFAIK, prawie nigdy nie widać prawdziwych ataków 2. rzędu, jak to zazwyczaj atakującym łatwiej jest zaimplementować się w społeczności, ale czasami pojawiają się błędy drugiego rzędu z powodu dodatkowych łagodnych znaków ' lub podobnych.

Można wykonać atak iniekcji drugiego rzędu, gdy można spowodować, że wartość zostanie zapisana w bazie danych, która jest później używana jako literał w zapytaniu. Jako przykład, załóżmy, że wprowadzasz następujące informacje jako nową nazwę Użytkownika podczas tworzenia konta na stronie internetowej (zakładając MySQL DB dla tego pytania):

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

Jeśli nie ma innych ograniczeń co do nazwy użytkownika, instrukcja prepared nadal upewniałaby się, że powyższe osadzone zapytanie nie zostanie wykonane w momencie wstawiania i poprawnie zapisze wartość w bazie danych. Wyobraź sobie jednak, że później aplikacja pobiera Twoją nazwę Użytkownika z bazy danych i używa konkatenacji łańcuchowej, aby dołączyć tę wartość do nowego zapytania. Możesz zobaczyć hasło kogoś innego. Ponieważ kilka pierwszych imion w tabeli użytkowników jest zazwyczaj administratorami, być może właśnie podałeś z dala od farmy. (Uwaga: jest to jeszcze jeden powód, aby nie przechowywać haseł w zwykłym tekście!)

Widzimy więc, że przygotowane instrukcje wystarczą na pojedyncze zapytanie, ale same w sobie nie są wystarczające do ochrony przed atakami SQL injection w całej aplikacji, ponieważ nie mają mechanizmu wymuszającego, aby cały dostęp do bazy danych w aplikacji używał bezpiecznego kodu. Jednak stosowane jako część 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ług, która ogranicza dynamiczny sql - przygotowane Oświadczeniapodstawowym narzędziem do rozwiązywania problemu SQL Injection. jeśli stosujesz dobre zasady projektowania aplikacji, takie że dostęp do danych jest oddzielony od reszty programu, łatwo jest wymusić lub skontrolować, że każde zapytanie poprawnie wykorzystuje parametryzację. W tym przypadku SQL injection (zarówno pierwszego, jak i drugiego rzędu) jest całkowicie / align = "left" /


*okazuje się, że MySql / PHP są (ok, były) po prostu głupi o obsłudze parametrów, gdy szerokie znaki są zaangażowane, i nadal istnieje Rzadki przypadek opisany w inne wysoko głosowane odpowiedź tutaj, które mogą pozwolić injection prześlizgnąć się przez parametryzowane zapytanie.

 496
Author: Joel Coehoorn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-07-18 16:53:12

Nie, Nie zawsze są.

Zależy to od tego, czy użytkownik zezwala na umieszczenie danych wejściowych 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']) );

Będzie podatny na iniekcje SQL i użycie gotowych instrukcji w tym przykładzie nie zadziała, ponieważ dane wejściowe użytkownika są używane jako identyfikator, a nie jako dane. Poprawną odpowiedzią byłoby użycie pewnego rodzaju filtrowania / walidacji, takiego 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żesz używać PDO do wiązania danych, które wykraczają 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ższe nie działa, jest to, że DESC i ASC nie są danymi . PDO może uciec tylko dla danych . Po drugie, nie można nawet umieścić ' cytatów wokół niego. Jedynym sposobem na umożliwienie sortowania wybranego przez użytkownika jest ręczne filtrowanie i sprawdzanie, czy jest to DESC lub ASC.

 40
Author: Tower,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-27 13:20:25

Tak, wystarczy. Sposób, w jaki ataki typu injection działają, polega na tym, że jakiś interpreter (baza danych) oceni coś, co powinno być danymi, jakby to był Kod. Jest to możliwe tylko wtedy, gdy mieszasz kod i dane na tym samym nośniku (np. podczas konstruowania zapytania jako ciąg znaków).

Parametryzowane zapytania działają wysyłając kod i dane oddzielnie, więc nigdy nie będzie można znaleźć dziury w tym.

Nadal możesz być podatny na inne ataki typu wtrysku. Na przykład, jeśli używasz danych na stronie HTML, możesz być narażony na ataki typu XSS.

 24
Author: troelskn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-25 15:55:46

Nie to nie wystarczy (w niektórych szczególnych przypadkach)! Domyślnie PDO używa emulowanych gotowych instrukcji, gdy używa MySQL jako Sterownika bazy danych. Podczas korzystania z MySQL i PDO należy zawsze wyłączyć emulowane instrukcje:

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

Kolejna rzecz, którą zawsze należy zrobić to ustawić poprawne kodowanie 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 wtrysku SQL w PHP?

Zwróć również uwagę, że dotyczy to tylko Strony bazy danych rzeczy, które nadal trzeba uważać podczas wyświetlania danych. Np. poprzez ponowne użycie htmlspecialchars() z poprawnym kodowaniem i stylem cytowania.

 24
Author: PeeHaa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:48

Osobiście zawsze najpierw uruchamiałbym jakąś formę sanitacji na danych, ponieważ nigdy nie można ufać wejściom użytkownika, jednak przy użyciu wiążących elementów zastępczych / parametrów wprowadzone dane są wysyłane do serwera oddzielnie do instrukcji sql, a następnie łączone razem. Kluczem tutaj jest to, że wiąże dostarczone dane do określonego typu i określonego zastosowania i eliminuje możliwość zmiany logiki instrukcji SQL.

 9
Author: JimmyJ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-25 15:50:43

Eaven jeśli zamierzasz zapobiec front-endowi SQL injection, używając sprawdzeń html lub js, musisz wziąć pod uwagę, że sprawdzania front-end są "pomijalne".

Możesz wyłączyć js lub edytować wzorzec za pomocą narzędzia programistycznego (wbudowanego obecnie w firefox lub chrome).

Tak więc, aby zapobiec SQL injection, byłoby prawo do dezynfekcji backend daty wejścia wewnątrz kontrolera.

Chciałbym zaproponować Ci użycie natywnej funkcji PHP filter_input () w celu dezynfekcja wartości GET I INPUT.

Jeśli chcesz przejść do bezpieczeństwa, dla sensownych zapytań do bazy danych, chciałbym zasugerować Ci użycie wyrażenia regularnego do walidacji formatu danych. preg_match () pomoże Ci w tym przypadku! Ale uważaj na siebie! 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 if(!preg_match('/[0-9]{1,2}/')){...} is heavyer of

if (isset($value) && intval($value)) <99) {...}
Ostateczna odpowiedź brzmi: "nie! PDO prepared Statements does not prevent all kind of SQL injection"; nie zapobiega nieoczekiwanym wartościom, tylko nieoczekiwanej konkatenacji
 -2
Author: snipershady,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-04 20:17:56