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:
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
, gbk
i sjis
. gbk
Tutaj wybierzemy .
Teraz bardzo ważne jest, aby zauważyć użycie SET NAMES
tutaj. To ustawia zestaw znaków NA SERWERZE . Jest na to inny sposób, ale niedługo się tam dostaniemy.
Ładunek
Ładunek, którego użyjemy do tego wstrzyknięcia, zaczyna się od sekwencji bajtów 0xbf27
. W gbk
to niepoprawny znak wielobajtowy; w latin1
to jest ciąg ¿'
. Zauważ, że w latin1
i gbk
, 0x27
na własną rękę jest dosłowny '
charakter.
Wybraliśmy ten ładunek, ponieważ jeśli addslashes()
go wywołamy, wstawimy ASCII, \
tj. 0x5c
Przed '
znakiem. Więc chcemy skończyć z 0xbf5c27
, która gbk
jest sekwencją dwóch znaków: 0xbf5c
nastę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 ...
$ 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 latin1
do 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ć $var
w gbk
zestawie znaków, to widzimy:
OR „LUB 1 = 1 / *
Właśnie tego wymaga atak.
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 PDO
nie 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_ESCAPES
tryb SQL, który (między innymi) zmienia działanie mysql_real_escape_string()
. Po włączeniu tego trybu 0x27
zostanie zastąpione 0x2727
zamiast zamiast, 0x5c27
dlatego 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. 0xbf27
Nadal jest 0xbf27
itp.) - 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_ESCAPES
tryb 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ą).