Wstrzyknięcie SQL, które omija mysql_real_escape_string ()

645

Czy istnieje możliwość wstrzyknięcia SQL, nawet przy użyciu mysql_real_escape_string()funkcji?

Rozważ tę przykładową sytuację. SQL jest zbudowany w PHP w następujący sposób:

$login = mysql_real_escape_string(GetFromPost('login'));
$password = mysql_real_escape_string(GetFromPost('password'));

$sql = "SELECT * FROM table WHERE login='$login' AND password='$password'";

Słyszałem, że wiele osób mówi mi, że taki kod jest nadal niebezpieczny i można go zhakować nawet przy użyciu mysql_real_escape_string()funkcji. Ale nie mogę wymyślić żadnego możliwego exploita?

Klasyczne zastrzyki takie jak to:

aaa' OR 1=1 --

nie działa.

Czy znasz jakieś możliwe zastrzyki, które mogłyby przejść przez powyższy kod PHP?

Richard Knop
źródło
34
@ ThiefMaster - Wolę nie podawać pełnych błędów, takich jak nieprawidłowy użytkownik / nieprawidłowe hasło ... informuje sprzedawców brutalnej siły, że mają prawidłowy identyfikator użytkownika, i to tylko hasło, które muszą odgadnąć
Mark Baker
18
Jest to jednak okropne z punktu widzenia użyteczności. Czasami nie można użyć głównego pseudonimu / nazwy użytkownika / adresu e-mail i zapomnieć o tym po pewnym czasie lub witryna usunęła konto z powodu braku aktywności. To jest bardzo irytujące, jeśli nadal próbujesz haseł, a może nawet zablokujesz swój adres IP, mimo że to tylko nazwa użytkownika jest niepoprawna.
ThiefMaster 21.04.2011
50
Proszę nie używać mysql_*funkcji w nowym kodzie . Nie są już utrzymywane irozpoczął się proces amortyzacji. Widzisz czerwone pole ? Dowiedz sięzamiast tegoo przygotowanych instrukcjach i użyj PDO lub MySQLi - ten artykuł pomoże ci zdecydować, które. Jeśli wybierzesz PDO, oto dobry tutorial .
tereško
13
@machineaddict, od wersji 5.5 (która została niedawno wydana) mysql_*funkcje już generują E_DEPRECATEDostrzeżenie. ext/mysqlRozszerzenie nie zostało utrzymane przez okres dłuższy niż 10 lat. Czy naprawdę jesteś tak złudny?
tereško
13
@machineaddict Właśnie usunęli to rozszerzenie w PHP 7.0 i nie jest jeszcze w 2050 roku.
GGG

Odpowiedzi:

379

Rozważ następujące zapytanie:

$iId = mysql_real_escape_string("1 OR 1=1");    
$sSql = "SELECT * FROM table WHERE id = $iId";

mysql_real_escape_string()nie ochroni cię przed tym. Chroni cię przed tym fakt, że używasz pojedynczych cudzysłowów ( ' ') wokół zmiennych w zapytaniu. Następująca jest również opcja:

$iId = (int)"1 OR 1=1";
$sSql = "SELECT * FROM table WHERE id = $iId";
Wesley van Opdorp
źródło
9
Ale to nie byłby prawdziwy problem, ponieważ mysql_query()nie wykonuje wielu instrukcji, nie?
Pekka,
11
@Pekka, Chociaż zwykłym przykładem jest DROP TABLE, w praktyce atakujący jest bardziej prawdopodobne SELECT passwd FROM users. W tym drugim przypadku drugie zapytanie jest zwykle wykonywane za pomocą UNIONklauzuli.
Jacco
58
(int)mysql_real_escape_string- to nie ma sensu. Nie różni (int)się wcale. I przyniosą ten sam wynik dla każdego wkładu
zerkms
28
Jest to bardziej niewłaściwe użycie tej funkcji niż cokolwiek innego. W końcu to się nazywa mysql_real_escape_string, a nie mysql_real_escape_integer. Nie należy go stosować z polami liczb całkowitych.
NullUserException
11
@ircmaxell, Jednak odpowiedź jest całkowicie myląca. Oczywiście pytanie dotyczy treści zawartych w cytatach. „Cytatów nie ma” nie jest odpowiedzią na to pytanie.
Pacerier
629

Krótka odpowiedź brzmi: tak, tak jest sposób na obejściemysql_real_escape_string() .

Do bardzo OBECURE CASE CASE !!!

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

Atak

Zacznijmy więc od pokazania ataku ...

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

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

  1. Wybieranie zestawu znaków

    mysql_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 . Gdybyśmy użyli wywołania funkcji C API mysql_set_charset(), wszystko byłoby w porządku (w wersjach MySQL od 2006 roku). Ale więcej o tym, dlaczego za minutę ...

  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. mysql_real_escape_string ()

    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 zaatakowałeś program za pomocą mysql_real_escape_string()...

Źli

Pogarsza się. PDOdomyślnie emuluje przygotowane instrukcje za pomocą MySQL. Oznacza to, że po stronie klienta wykonuje on sprintf mysql_real_escape_string()(w bibliotece C), co oznacza, że ​​następujące działania zakończą się powodzeniem:

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

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).

Brzydki

Na samym początku powiedziałem, że moglibyśmy temu zapobiec, gdybyśmy tego użyli mysql_set_charset('gbk')zamiast SET NAMES gbk. I to prawda, pod warunkiem, że używasz 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 dostępny jako parametr DSN .

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 ataki, 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 wynikać z używania tego trybu SQL.

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 mysql_set_charset() / $mysqli->set_charset()/ PDO parametr zestawu znaków DSN (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)

Jesteś w 100% bezpieczny.

W przeciwnym razie jesteś podatny na zagrożenia, nawet jeśli używaszmysql_real_escape_string() ...

ircmaxell
źródło
3
Emulowanie PDO przygotowuje instrukcje dla MySQL, naprawdę? Nie widzę powodu, dla którego miałby to robić, ponieważ sterownik obsługuje go natywnie. Nie?
netcoder
16
To robi. Mówią w dokumentacji, że tak nie jest. Ale w kodzie źródłowym jest to wyraźnie widoczne i łatwe do naprawienia. Piszę to do niekompetencji twórców.
Theodore R. Smith
5
@ TheodoreR.Smith: Naprawienie tego nie jest takie łatwe. Pracowałem nad zmianą wartości domyślnej, ale po przełączeniu nie udaje jej się załadować łodzi. To większa zmiana, niż się wydaje. Nadal mam nadzieję, że skończę to do 5,5 ...
ircmaxell,
14
@shadyyx: Nie, podatność opisana w tym artykule addslashes. I na podstawie tej podatności na jednym. Spróbuj sam. Pobierz MySQL 5.0, uruchom ten exploit i przekonaj się sam. Jeśli chodzi o to, jak umieścić to w PUT / GET / POST, to jest TRIVIAL. Dane wejściowe to tylko bajty strumieniowe. char(0xBF)to tylko czytelny sposób generowania bajtu. Pokazałem tę lukę na żywo przed wieloma konferencjami. Zaufaj mi w tym ... Ale jeśli nie, spróbuj sam. Działa ...
ircmaxell,
5
@shadyyx: Co do przekazywania takiej funkcjonalności w $ _GET ... ?var=%BF%27+OR+1=1+%2F%2Aw adresie URL, $var = $_GET['var'];w kodzie, a Bob jest twoim wujem.
cHao
183

TL; DR

mysql_real_escape_string()nie zapewni żadnej ochrony (a ponadto może zniszczyć twoje dane), jeśli:

  • NO_BACKSLASH_ESCAPESTryb 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 testtabeli. Rozbiór:

  1. 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.

  2. Ładunek

    " OR 1=1 -- 

    Ładunek inicjuje ten zastrzyk dosłownie z "postacią. Bez szczególnego kodowania. Brak znaków specjalnych. Żadnych dziwnych bajtów.

  3. 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_ESCAPESuż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 $varszczą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 .

  4. 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_ESCAPEStrybie 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_ESCAPESmoż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ą SUPERuż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_ESCAPESlub 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_ESCAPESrównież włącza ANSI_QUOTEStryb, 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

  • w uzupełnieniu do zatrudniania jednym z rozwiązań w ircmaxell za Podsumowując, należy użyć co najmniej jednego z:

    • ChNP;
    • jednoznaczne literały łańcuchowe; lub
    • jawnie ustawiony tryb SQL, który nie obejmuje NO_BACKSLASH_ESCAPES

... powinieneś być całkowicie bezpieczny (luki poza zakresem uciekania łańcucha).

Eggyal
źródło
10
Tak więc TL; DR byłoby jak „istnieje tryb serwera mysql NO_BACKSLASH_ESCAPES, który może spowodować zastrzyk, jeśli nie używasz pojedynczych cudzysłowów.
Twój zwykły zmysł
Nie mogę uzyskać dostępu do bugs.mysql.com/bug.php?id=72458 ; Właśnie dostałem stronę, której odmówiono dostępu. Czy jest to ukryte przed opinią publiczną z powodu problemów związanych z bezpieczeństwem? Czy na podstawie tej odpowiedzi rozumiem również poprawnie, że jesteś odkrywcą luki w zabezpieczeniach? Jeśli tak, gratulacje.
Mark Amery
1
Ludzie nie powinni "przede wszystkim używać ciągów znaków. SQL mówi, że to dla identyfikatorów. Ale eh ... tylko kolejny przykład MySQL-a mówiącego: „śrubuj standardy, zrobię, co chcę”. (Na szczęście możesz włączyć ANSI_QUOTEStryb poprawiania cytowania. Otwarte lekceważenie standardów jest jednak większym problemem, który może wymagać surowszych środków.)
cHao
2
@ DanAllen: moja odpowiedź była nieco szersza, ponieważ można uniknąć tego konkretnego błędu dzięki quote()funkcji PDO - ale przygotowane oświadczenia są znacznie bezpieczniejszym i bardziej odpowiednim sposobem na uniknięcie zastrzyku. Oczywiście, jeśli bezpośrednio połączyłeś zmienne nieskalowane z SQL, to z pewnością jesteś podatny na wstrzykiwanie, bez względu na to, jakich metod użyjesz później.
eggyal
1
@eggyall: Nasz system opiera się na drugim bezpiecznym przykładzie powyżej. Występują błędy, w których pominięto ciąg mysql_real_escape_string. Naprawianie ich w trybie awaryjnym wydaje się być rozsądną ścieżką, mając nadzieję, że nie zostaniemy ostrzeżeni przed poprawkami. Moje uzasadnienie przejścia na przygotowane oświadczenia będzie znacznie dłuższym procesem, który będzie musiał nastąpić później. Czy powodem, dla którego przygotowane oświadczenia są bezpieczniejsze, jest fakt, że błędy nie tworzą luk w zabezpieczeniach? Innymi słowy, czy poprawnie wdrożony 2. przykład powyżej jest tak samo bezpieczny jak przygotowane oświadczenia?
DanAllen
18

Cóż, tak naprawdę nic nie może przez to przejść, oprócz %symboli wieloznacznych. Może to być niebezpieczne, jeśli użyjesz LIKEinstrukcji, ponieważ atakujący może podać dane %logowania, jeśli nie odfiltrujesz tego i będziesz musiał po prostu brutalnie wymusić hasło któregokolwiek z twoich użytkowników. Ludzie często sugerują użycie przygotowanych instrukcji, aby były w 100% bezpieczne, ponieważ dane nie mogą w ten sposób zakłócać samego zapytania. Ale w przypadku takich prostych zapytań prawdopodobnie bardziej efektywne byłoby zrobienie czegoś takiego$login = preg_replace('/[^a-zA-Z0-9_]/', '', $login);

Slava
źródło
2
+1, ale symbole wieloznaczne dotyczą klauzuli LIKE, a nie zwykłej równości.
Dor
7
Jakim sposobem uważasz za prostą zamianę more efficientniż używanie przygotowanych wyciągów? (Przygotowane instrukcje zawsze działają, biblioteka może być szybko poprawiona w przypadku ataków, nie ujawnia błędu ludzkiego [takiego jak błędne wpisanie całego ciągu zastępującego] i ma znaczące korzyści w zakresie wydajności, jeśli instrukcja zostanie ponownie użyta.)
MatBailie,
7
@Slava: Skutecznie ograniczasz nazwy użytkowników i hasła tylko do znaków słownych. Większość ludzi, którzy wiedzą coś o bezpieczeństwie, uznałaby to za zły pomysł, ponieważ znacznie zmniejsza przestrzeń wyszukiwania. Oczywiście uznaliby również za zły pomysł przechowywanie haseł w postaci czystego tekstu w bazie danych, ale nie musimy pogłębiać problemu. :)
cHao
2
@ cHao, moja sugestia dotyczy tylko logowania. Oczywiście nie musisz filtrować haseł, przepraszam, że nie jest to wyraźnie określone w mojej odpowiedzi. Ale tak naprawdę może to być dobry pomysł. Używanie „kamiennej ignoranckiej przestrzeni drzewa” zamiast trudnego do zapamiętania i wpisania „a4üua3! @V \" ä90; 8f "byłoby znacznie trudniejsze do brutalnej siły. Nawet używając słownika, powiedz 3000 słów, aby ci pomóc, wiedząc użyłeś dokładnie 4 słów - nadal byłoby to około 3,3 * 10 ^ 12 kombinacji. :)
Slava
2
@Slava: Widziałem już ten pomysł; patrz xkcd.com/936 . Problem w tym, że matematyka nie do końca to znosi. Twoje przykładowe 17-znakowe hasło miałoby 96 ^ 17 możliwości, i to jeśli zapomniałeś umlautów i ograniczyłeś się do ASCII do wydruku. To około 4,5x10 ^ 33. Mówimy dosłownie miliard trylionów razy więcej pracy na brutalną siłę. Nawet 8-znakowe hasło ASCII miałoby 7,2 x 10 ^ 15 możliwości - 3 tysiące razy więcej.
cHao