TL; DR
mysql_real_escape_string()
nie zapewni żadnej ochrony (a ponadto może zniszczyć twoje dane), jeśli:
NO_BACKSLASH_ESCAPES
Tryb SQL MySQL jest włączony (który może być, chyba że jawnie wybierzesz inny tryb SQL przy każdym połączeniu ); i
literały ciągów SQL są cytowane przy użyciu "
znaków cudzysłowu .
Został on zgłoszony jako błąd nr 72458 i został naprawiony w MySQL v5.7.6 (patrz sekcja zatytułowana „ The Saving Grace ” poniżej).
To kolejna (może mniej?) Niejasna KRAWĘDZIA EDGE !!!
W hołdzie doskonałej odpowiedzi @ ircmaxell (naprawdę, to ma być pochlebstwo, a nie plagiat!), Przyjmuję jego format:
Atak
Zaczynam od demonstracji ...
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"'); // could already be set
$var = mysql_real_escape_string('" OR 1=1 -- ');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Spowoduje to zwrócenie wszystkich rekordów z test
tabeli. Rozbiór:
Wybór trybu SQL
mysql_query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
Jak udokumentowano pod Smyczkowych literale :
Istnieje kilka sposobów umieszczania znaków cudzysłowu w ciągu:
„ '
” Wewnątrz ciągu cytowanego z „ '
” może być zapisane jako „ ''
”.
„ "
” Wewnątrz ciągu cytowanego z „ "
” może być zapisane jako „ ""
”.
Poprzedz cudzysłów znakiem ucieczki („ \
”).
„ '
” Wewnątrz ciągu cytowanego z „ "
” nie wymaga specjalnego traktowania i nie trzeba go podwójnie ani uciekać. W ten sam sposób „ "
” wewnątrz ciągu cytowanego z „ '
” nie wymaga specjalnego traktowania.
Jeśli zawiera tryb SQL serwera NO_BACKSLASH_ESCAPES
, wówczas trzecia z tych opcji - co jest typowym podejściem przyjętym przez mysql_real_escape_string()
- nie jest dostępna: zamiast niej należy użyć jednej z dwóch pierwszych opcji. Zauważ, że efekt czwartego pocisku polega na tym, że trzeba koniecznie znać znak, który zostanie użyty do zacytowania literału, aby uniknąć mungowania danych.
Ładunek
" OR 1=1 --
Ładunek inicjuje ten zastrzyk dosłownie z "
postacią. Bez szczególnego kodowania. Brak znaków specjalnych. Żadnych dziwnych bajtów.
mysql_real_escape_string ()
$var = mysql_real_escape_string('" OR 1=1 -- ');
Na szczęście mysql_real_escape_string()
sprawdza tryb SQL i odpowiednio dostosowuje jego zachowanie. Zobacz libmysql.c
:
ulong STDCALL
mysql_real_escape_string(MYSQL *mysql, char *to,const char *from,
ulong length)
{
if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES)
return escape_quotes_for_mysql(mysql->charset, to, 0, from, length);
return escape_string_for_mysql(mysql->charset, to, 0, from, length);
}
W ten sposób escape_quotes_for_mysql()
wywoływana jest inna funkcja bazowa, jeśli NO_BACKSLASH_ESCAPES
używany jest tryb SQL. Jak wspomniano powyżej, taka funkcja musi wiedzieć, który znak zostanie użyty do zacytowania literału, aby powtórzyć go bez powodowania dosłownego powtórzenia drugiego znaku cytowania.
Jednak ta funkcja arbitralnie zakłada, że ciąg będzie cytowany przy użyciu '
znaku pojedynczego cudzysłowu . Zobacz charset.c
:
/*
Escape apostrophes by doubling them up
// [ deletia 839-845 ]
DESCRIPTION
This escapes the contents of a string by doubling up any apostrophes that
it contains. This is used when the NO_BACKSLASH_ESCAPES SQL_MODE is in
effect on the server.
// [ deletia 852-858 ]
*/
size_t escape_quotes_for_mysql(CHARSET_INFO *charset_info,
char *to, size_t to_length,
const char *from, size_t length)
{
// [ deletia 865-892 ]
if (*from == '\'')
{
if (to + 2 > to_end)
{
overflow= TRUE;
break;
}
*to++= '\'';
*to++= '\'';
}
Tak więc pozostawia "
nietknięte znaki podwójnego cudzysłowu (i podwaja wszystkie '
znaki pojedynczego cudzysłowu ), niezależnie od rzeczywistego znaku używanego do cytowania literału ! W naszym przypadku $var
szczątków dokładnie taka sama jak argument, który został przewidziany do mysql_real_escape_string()
-To jakby ma ucieczki doszło w ogóle .
Zapytanie
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
Renderowane zapytanie jest formalnością:
SELECT * FROM test WHERE name = "" OR 1=1 -- " LIMIT 1
Jak ujął to mój wyuczony przyjaciel: gratulacje, właśnie zaatakowałeś program za pomocą mysql_real_escape_string()
...
Źli
mysql_set_charset()
nie może pomóc, ponieważ nie ma to nic wspólnego z zestawami znaków; też nie może mysqli::real_escape_string()
, ponieważ jest to tylko inne opakowanie wokół tej samej funkcji.
Problem, o ile jeszcze nie jest oczywisty, polega na tym, że wezwanie do mysql_real_escape_string()
nie może wiedzieć, z jaką literą będzie cytowany literał, ponieważ programista musi zdecydować później. Tak więc w NO_BACKSLASH_ESCAPES
trybie dosłownie nie ma sposobu, aby ta funkcja mogła bezpiecznie uciec przed każdym wejściem w celu użycia z arbitralnym cytowaniem (przynajmniej bez podwojenia znaków, które nie wymagają podwajania, a tym samym mungowania danych).
Brzydki
Pogarsza się. NO_BACKSLASH_ESCAPES
może nie być wcale tak rzadkie na wolności ze względu na konieczność użycia go do kompatybilności ze standardowym SQL (np. patrz sekcja 5.3 specyfikacji SQL-92 , a mianowicie <quote symbol> ::= <quote><quote>
produkcja gramatyki i brak jakiegokolwiek specjalnego znaczenia nadanego odwrotnemu ukośnikowi). Co więcej, jego użycie zostało wyraźnie zalecane jako obejście (dawno naprawionego) błędu opisanego w poście ircmaxell. Kto wie, niektóre DBA mogą nawet skonfigurować go tak, aby był domyślnie włączony, aby zniechęcić do używania niewłaściwych metod zmiany znaczenia addslashes()
.
Ponadto tryb SQL nowego połączenia jest ustawiany przez serwer zgodnie z jego konfiguracją (którą SUPER
użytkownik może zmienić w dowolnym momencie); dlatego, aby być pewnym zachowania serwera, należy zawsze wyraźnie określić pożądany tryb po połączeniu.
The Saving Grace
Tak długo, jak zawsze jawnie ustawiasz tryb SQL, aby nie dołączać NO_BACKSLASH_ESCAPES
lub cytować literałów łańcuchowych MySQL za pomocą znaku pojedynczego cudzysłowu, ten błąd nie może odwrócić swojej brzydkiej głowy: odpowiednio escape_quotes_for_mysql()
nie zostanie użyty, lub jego założenie o tym, które znaki cytatu wymagają powtarzania, będzie być poprawnym.
Z tego powodu zalecam, aby każdy, kto używa, NO_BACKSLASH_ESCAPES
również włącza ANSI_QUOTES
tryb, ponieważ wymusi zwykłe używanie literałów ciągowych w cudzysłowie. Zauważ, że nie zapobiega to iniekcji SQL w przypadku użycia literałów podwójnie cytowanych - ogranicza jedynie prawdopodobieństwo takiego zdarzenia (ponieważ nie powiodłyby się normalne, nie złośliwe zapytania).
W PDO PDO::quote()
wywoływana jest zarówno jego równoważna funkcja, jak i przygotowany emulator instrukcji mysql_handle_quoter()
- co robi dokładnie to: zapewnia, że literał, który uciekł, jest cytowany w pojedynczych cudzysłowach, dzięki czemu można mieć pewność, że PDO jest zawsze odporny na ten błąd.
Od wersji MySQL v5.7.6 ten błąd został naprawiony. Zobacz dziennik zmian :
Funkcjonalność dodana lub zmieniona
Bezpieczne przykłady
W połączeniu z błędem wyjaśnionym przez ircmaxell, poniższe przykłady są całkowicie bezpieczne (zakładając, że albo używa MySQL później niż 4.1.20, 5.0.22, 5.1.11; albo że nie używa kodowania połączenia GBK / Big5) :
mysql_set_charset($charset);
mysql_query("SET SQL_MODE=''");
$var = mysql_real_escape_string('" OR 1=1 /*');
mysql_query('SELECT * FROM test WHERE name = "'.$var.'" LIMIT 1');
... ponieważ jawnie wybraliśmy tryb SQL, który nie obejmuje NO_BACKSLASH_ESCAPES
.
mysql_set_charset($charset);
$var = mysql_real_escape_string("' OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
... ponieważ cytujemy nasz literał łańcuchowy pojedynczymi cudzysłowami.
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(["' OR 1=1 /*"]);
... ponieważ instrukcje przygotowane przez PDO są odporne na tę lukę (i ircmaxell również, pod warunkiem, że używasz PHP≥5.3.6 i zestaw znaków został poprawnie ustawiony w DSN; lub że emulacja przygotowanej instrukcji została wyłączona) .
$var = $pdo->quote("' OR 1=1 /*");
$stmt = $pdo->query("SELECT * FROM test WHERE name = $var LIMIT 1");
... ponieważ quote()
funkcja PDO nie tylko wymyka się dosłowności, ale także ją cytuje (w postaci pojedynczych cudzysłowów '
); Pamiętaj, że aby uniknąć ircmaxell za błąd w tym przypadku musi być za pomocą PHP≥5.3.6 i poprawnie ustawić zestaw znaków w DSN.
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "' OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
... ponieważ instrukcje przygotowane przez MySQLi są bezpieczne.
Podsumowanie
Zatem jeśli:
- używaj natywnie przygotowanych instrukcji
LUB
- użyj MySQL w wersji 5.6.6 lub nowszej
LUB
... powinieneś być całkowicie bezpieczny (luki poza zakresem uciekania łańcucha).