W pewnym momencie tworzenia skryptów używających bazy danych w PHP, programista zaczyna się zastanawiać czy czasem o czymś nie zapomniał, lub zauważa swój błąd po ataku na witrynę. Jednym z najczęściej spotykanych sposobów włamań jest tzn. SQL Injection, czyli po prostu wstrzyknięcie złośliwego kodu. Bardzo często spotyka się je w niezabezpieczonych formularzach, można również doszukać się ich w systemie adresowania, który ma sprzyjać robotom sieciowym. W wyniku ataku można stracić sporo, w szczególności wtedy, gdy włamywacz nie zniszczy materiałów (a co za tym idzie, nie ujawni administratorowi faktu włamania), a tylko wykradnie pewne interesujące go dane zostawiając furtkę cały czas otwartą. Są na to jednak sprawdzone sposoby, których wdrożenie to chwila warta poświęcenia uwagi.

Czym jest SQL Injection?
Jak napisałem wcześniej, oznacza to po prostu wstrzyknięcie kodu. Niestety wielu osobom może niewiele to mówić, więc przeanalizujmy konkretny przykład. Załóżmy, że mamy na stronie wyszukiwarkę użytkowników – wklepujemy nick w odpowiednie pola formularza, naciskamy ok i otrzymujemy wyniki. Proste, nieprawdaż? Kod podatny na atak tego typu może wyglądać na przykład tak tak:

$nick = $_POST['nick'];
$sql = "SELECT user_id, user_age, user_location
        FROM uzytkownicy
        WHERE user_nick = '$nick'";
$query = mysql_query($sql);
// Dalsza cześć kodu

Wygląda niewinnie, nieprawdaż? No dobra, ale co się stanie, jeżeli w pole z nickiem ktoś wpisze coś takiego:

';DELETE FROM uzytkownicy;SELECT user_id FROM uzytkownicy WHERE user_nick = '

Zapytanie, które skonstruowaliśmy w PHP zostanie delikatnie mówiąc zmienione: pojawią się dwa nowe, z którego jedno na pewno nie będzie dla nas fascynujące:

SELECT user_id, user_age, user_location FROM uzytkownicy WHERE user_nick = '';
DELETE FROM uzytkownicy;
SELECT user_id FROM uzytkownicy WHERE user_nick = '';

Początek kodu uzupełnia nasze zapytanie i je kończy. Następnie wykonywane jest główne zapytanie powodujące straty po naszej stronie – usunięcie wszystkich rekordów z tabeli uzytkownicy. Na końcu znajduje się niedokończone zapytanie, które ponownie dopasowuje się do naszego kodu w PHP. Oczywiście to tylko przykład. Zamiast usuwać, można wykonywać dowolne zapytania: zmienić tabele, dodawać rekordy czy wyciągać interesujące dane (oczywiście o ile pozwalają na to uprawnienia danego użytkownika bazy). Co więc było nie tak? Zapomnieliśmy o sprawdzaniu danych przyjmowanych z formularza. Mamy na to kilka sposobów.

HTML
Dosyć dobrym rozwiązaniem jest ograniczenie długości pola input w formularzu – przecież raczej nikt nie ma nicka zawierającego np. 40 znaków. Niestety nie zawsze się to sprawdza. W podanym przykładzie z nickiem będzie ok, ale co w innych wypadkach? Czasami możemy szukać naprawdę długich wartości, nawet jeżeli są typu string. Musimy dokonać walidacji z poziomu PHP.

Dyrektywa magic_quotes_gpc
W konfiguracji PHP mamy do czynienia z czymś takim jak dyrektywa magic_quotes_gpc. Jeżeli wartość ta jest włączona (1 lub on), ciągi z tablic superglobalnych są automatycznie zabezpieczane przez dodanie backslashów przed niebezpiecznymi znakami. Osobiście nie uważam jednak tej metody za dobrą: nie warto polegać na ustawieniach serwera, bo po jego zmienić wszystko może się zmienić. Warto więc wyłączyć dyrektywę jeżeli jest aktywna i skorzystać z innych metod, dzięki którym nasz kod stanie się bardziej uniwersalny.

Funkcja mysql_real_escape_string();
Metodą zalecaną przez podręcznik PHP jest oczyszczanie wprowadzanych ciągów za pomocą funkcji mysql_real_escape_string(string), której argumentem jest ciąg do zabezpieczenia. Dodaje ona przed wszelkimi niebezpiecznymi znakami takimi jak apostrofy i cudzysłowy backslash. W tym wypadku poprawiony kod wyglądałby tak:

$nick = mysql_real_escape_string($_POST['nick']);
$sql = "SELECT user_id, user_age, user_location
        FROM uzytkownicy
        WHERE user_nick = '$nick'";
$query = mysql_query($sql);
// Dalsza cześć kodu

Funkcja addslashes();
Niestety z pomiarów które wykonałem wynika, iż z powodu konieczności pobrania identyfikatora połączenia i dobrania odpowiedniego kodowania znaków, mysql_real_escape_string() wykonuje się dosyć długo, co będzie miało znaczenie w wypadku tworzenia przyjaznych linków. Znacznie szybciej, bo o około 95%, działa natomiast bardzo podobna funkcja addslashes(). Podobnie jak poprzednio, w argumencie podajemy ciąg do zabezpieczenia – wszelkie cudzysłowy i apostrofy stają się po przekształceniu całkowicie bezużyteczne:

$nick = addslashes($_POST['nick']);
$sql = "SELECT user_id, user_age, user_location
        FROM uzytkownicy
        WHERE user_nick = '$nick'";
$query = mysql_query($sql);
// Dalsza cześć kodu

Inne zalecenia
Ciągi możemy zabezpieczyć w opisany powyżej sposób. Inne wartości powinniśmy sprawdzać odpowiednimi funkcjami zawartymi w PHP. Jeżeli sprawdzamy id danego użytkownika, należy sprawdzić czy podana wartość jest numeryczna. Jeżeli nie, pominąć zapytanie i zgłosić odpowiedni błąd, oto przykład:

$user_id = $_POST['user_id']);
if(is_numeric($user_id))
{
  $sql = "SELECT user_nick, user_age, user_location
          FROM uzytkownicy
          WHERE user_id = '$user_id'";
  $query = mysql_query($sql);
}

Możemy również sami napisać funkcję, która będzie sprawdzała czy dane wejściowe np. zawierają takie wyrażenia jak SELECT, INSERT, DELETE itp. Czy jest to jednak potrzebne? Zasób funkcji PHP pokazuje, iż ponowne wynalezienie koła wcale nie jest potrzebne. Możemy natomiast nieco udoskonalić nasze zapytanie, traktując szukane dane jako element łączony:

$sql = "SELECT user_nick, user_age, user_location
        FROM uzytkownicy
        WHERE user_id = '" . $user_id . "'";

Z pewnością znajdziecie jeszcze inne sposoby – jest ich wiele. Nic nie stoi na przeszkodzie, aby połączyć niektóre z nich (np. sprawdzamy czy zmienna jest typu string, a potem dodajemy backslashe). Należy jednak o takim zabezpieczeniu pamiętać, bowiem SQL Injection to chyba najbardziej znany sposób włamania, które wynika z zaniedbania kodu. Na dodatek praktycznie każdy interaktywny serwis posiada jakieś formularze, które aż proszą się o sprawdzenie, czy programista odpowiednio filtruje dane wejściowe.

Podobne wpisy:

  1. Resetowanie i uzupełnanie id z AUTO INCREMENT
  2. Sposób na VBScript Error 2738

8 komentarzy

Dodaj komentarz
  1. Lisu   |   06.07.2009 20:53

    Pisząc trzeba pamiętać o właściwym zabezpieczeniu strony, ponieważ łatwo wszystko stracic.

  2. magnetic   |   15.07.2009 00:45

    pierwszy kod i jak dla mnie się zagapiłeś ;p
    $query = mysql_query($query);
    , a nie powinno być czasem
    $query = mysql_query($sql);

    Pozdrawiam!

  3. LukasAMD   |   15.07.2009 10:24

    No tak ;)
    Zrobiłem tak z przyzwyczajenia – często nie widzę sensu w robieniu nowych zmiennych, ale na potrzeby przykładu wolałem dać nieco mniej zagmatwany kod ;)

  4. tomek   |   15.07.2009 12:08

    A ja mam pytanie ograniczyłem sobie wielkość znaków ale jeśli pisze jakiś tekst i pokazuje mi się wybór słowa które już kiedyś pisałem to ładuje się do formularza nie patrząc na wielkość znaków jak to zabezpieczyć ?

  5. LukasAMD   |   15.07.2009 12:19

    Czym ograniczałeś? Jeżeli size, to nie da nic, bo to definiuje szerokość inputa. Ograniczenie w długości ustawia się poprzez atrybut maxlength – nawet jeżeli wrzuci zapamiętane, to albo przytnie je w formularzu, albo przy przesyłaniu poda tylko tyle znaków, ile ustawiłeś.

    Niemniej jednak nie polecam tego – kod do włamania nie musi być długi, więc metoda ta jest dosyć nieskuteczna.

  6. tomek   |   15.07.2009 13:15

    dałem maxlength ale jeśli mówisz że przytnie przy wysyłaniu to dobrze mam ustawione w szukające max 10 znaków takich zapytań chyba nie ma :D
    czyli najlepsze jest mysql_real_escape_string ?
    a gdzie tą funkcje w ef wrzucić przed opentable ? tu ?

    if (!isset($stype)) $stype = isset($_POST['stype']) ? $_POST['stype'] : „a”;
    if (!isset($stext)) $stext = isset($_POST['stext']) ? $_POST['stext'] : „”;

    chodzi o szukajke oczywiście

  7. Victor   |   15.07.2009 13:26

    Najlepszym sposobem na uniknięcie ogólnie Injectionów, jest:
    W przypadku integera – (int) przed zmienną (to samo co intval()),
    w przypadku stringa – napisać własną czyszczącą funkcję – str_replace, array i do dzieła. (zamienianie /, , ‘, ” po prostu na pustkę).

  8. LukasAMD   |   15.07.2009 13:27

    @tomek:
    Najlepiej najpierw zrobić konwersję z EF na PF7 – zarówno EF jak i PF6 korzystają z emulacji register_globals, co jest już całkowitą porażką w kwestii bezpieczeństwa. W PF7 tego nie ma.

    A kod IMO najlepiej w ten deseń:

    $stype = ”;
    $stext = ”;

    if (isset($_POST['stype'])) $stype = mysql_real_escape_string($_POST['stype']);
    else $stype = ‘a’;

    if (isset($_POST['stext'])) $stext = mysql_real_escape_string($_POST['stext']);
    else $stext = ”;

    @Victor:
    Wymuszanie zmiany na int’a ok, ale wymyślanie koła na nowo nie uważam za dobre rozwiązanie – lepiej użyć funkcji wbudowanych w PHP, bo zawsze działają szybciej. W wypadku addslashes możemy natomiast zrobić własną odmianę, która sprawdza najpierw czy mamy włączone magic_quotes_gpc (choć IMO lepiej i tak tą opcję wyłączać i wszystko filtrować).

Dodaj komentarz