Co to jest SQL Injection i jak mu zapobiegać w aplikacjach PHP?

Photo of author

By maciekx

Czy uważasz, że Twoja baza danych SQL jest szczytem wydajności i fortecą nie do zdobycia? Niestety, podatność na ataki typu SQL Injection może Cię zaskoczyć!

Tak, mówimy o potencjalnej katastrofie, gdyż nie chcemy używać oklepanych frazesów o „wzmacnianiu zabezpieczeń” i „ochronie przed złośliwymi intruzami”. SQL Injection to problem tak stary jak samo programowanie, więc każdy programista powinien znać go na wylot, a co za tym idzie, umieć mu przeciwdziałać. Jednak, jak to bywa, chwila nieuwagi może mieć fatalne konsekwencje.

Jeśli temat SQL Injection nie jest Ci obcy, możesz przejść do dalszej części tekstu. Jeśli jednak dopiero rozpoczynasz swoją przygodę z tworzeniem stron internetowych i aspirujesz do poważniejszych ról, krótkie wprowadzenie będzie jak znalazł.

Czym właściwie jest SQL Injection?

Kluczem do zrozumienia istoty ataku SQL Injection jest jego nazwa: SQL + Injection, czyli wstrzyknięcie kodu SQL. Choć termin „wstrzyknięcie” kojarzy się z medycyną, w tym przypadku chodzi o umieszczenie, wprowadzenie kodu SQL do aplikacji internetowej.

Wprowadzenie kodu SQL do aplikacji… czy nie tym właśnie się zajmujemy? Tak, ale w kontrolowany sposób. Nie chcemy przecież dać atakującemu władzy nad naszą bazą danych. Spójrzmy na przykład, aby lepiej zrozumieć, o co chodzi.

Załóżmy, że tworzysz prostą stronę w PHP dla lokalnego sklepu i chcesz dodać formularz kontaktowy:

<form action="record_message.php" method="POST">
  <label>Twoje imię</label>
  <input type="text" name="name">
  
  <label>Twoja wiadomość</label>
  <textarea name="message" rows="5"></textarea>
  
  <input type="submit" value="Wyślij">
</form>

Plik send_message.php ma za zadanie zapisać dane w bazie, by właściciele sklepu mogli odczytywać wiadomości. Oto przykładowy kod:

<?php

$name = $_POST['name'];
$message = $_POST['message'];

// sprawdzenie, czy użytkownik już wysłał wiadomość
mysqli_query($conn, "SELECT * from messages where name = $name");

// dalszy kod

Kod najpierw sprawdza, czy dany użytkownik ma już nieprzeczytaną wiadomość. Zapytanie SELECT * z tabeli messages, gdzie name = $name wydaje się być w porządku, prawda?

Błąd! Potężny błąd!

W ten sposób nieświadomie otwieramy drzwi do katastrofy. Atakujący może wykorzystać sytuację, jeśli zostaną spełnione następujące warunki:

  • Aplikacja korzysta z bazy danych SQL (obecnie to standard)
  • Bieżące połączenie z bazą danych ma uprawnienia do modyfikacji i usuwania danych
  • Można odgadnąć nazwy istotnych tabel

Ostatni punkt oznacza, że atakujący, wiedząc, że prowadzisz sklep internetowy, może przypuszczać, że dane o zamówieniach przechowujesz w tabeli o nazwie „orders”. Mając to wszystko, wystarczy, że jako imię poda następujący ciąg znaków:

Joe; TRUNCATE orders;? A potem patrz, jak wygląda zapytanie po przetworzeniu przez PHP:

SELECT * FROM messages WHERE name = Joe; TRUNCATE orders;

Pierwsza część zapytania ma błąd składni (brak cudzysłowów wokół „Joe”), jednak średnik wymusza na silniku MySQL rozpoczęcie interpretacji nowej instrukcji: TRUNCATE orders. W ten sposób, jednym prostym atakiem, cała historia zamówień zostaje usunięta!

Teraz, gdy już wiesz, jak działa SQL Injection, czas dowiedzieć się, jak mu zapobiegać. Dwa warunki niezbędne do skutecznego ataku to:

  • Skrypt PHP musi mieć uprawnienia do modyfikacji/usuwania danych w bazie. Niestety, to cecha większości aplikacji, i nie można ustawić bazy w trybie tylko do odczytu. Nawet jeśli usuniemy wszelkie uprawnienia do modyfikacji, SQL Injection nadal może umożliwić przeglądanie całej bazy danych, w tym poufnych informacji. Redukcja uprawnień nie rozwiązuje problemu, a aplikacja ich potrzebuje.
  • Przetwarzane są dane wprowadzone przez użytkownika. Jedynym sposobem na atak SQL Injection jest akceptowanie danych od użytkowników. Zablokowanie wszystkich danych wejściowych nie jest praktycznym rozwiązaniem.
  • Jak zapobiegać SQL Injection w PHP?

    Skoro połączenia z bazą, zapytania i dane od użytkowników są nieodzowne, jak możemy uniknąć SQL Injection? Na szczęście, jest to dość proste. Możemy to zrobić na dwa sposoby: 1) oczyszczać dane od użytkownika oraz 2) korzystać z przygotowanych zapytań.

    Oczyszczanie danych od użytkownika

    Jeśli używasz starszej wersji PHP (5.5 lub starszej, co często zdarza się na współdzielonych hostingach), powinieneś przepuścić wszystkie dane od użytkownika przez funkcję mysql_real_escape_string(). Jej zadaniem jest usunięcie znaków specjalnych z ciągu, tak by nie miały one specjalnego znaczenia dla bazy danych.

    Przykładowo, w ciągu „I’m string”, apostrof (’) może zostać wykorzystany przez atakującego do manipulacji zapytaniem. Użycie funkcji mysql_real_escape_string() zmieni ten ciąg na „I\’m string”, dodając ukośnik przed apostrofem, który go „uciekł”. W rezultacie, cały ciąg będzie traktowany jako nieszkodliwy tekst, a nie część manipulowanego zapytania.

    To rozwiązanie ma jednak wadę: jest to technika przestarzała, pasująca do starszych metod dostępu do bazy danych w PHP. Od PHP 7 ta funkcja już nie istnieje, co zmusza nas do szukania innych rozwiązań.

    Używanie przygotowanych zapytań

    Przygotowane zapytania to bezpieczniejsza i bardziej niezawodna metoda wykonywania zapytań do bazy danych. Zamiast wysyłać bezpośrednie zapytanie do bazy, informujemy ją najpierw o strukturze zapytania, które chcemy wykonać. To właśnie rozumiemy pod pojęciem „przygotowanie”. Po przygotowaniu zapytania, przesyłamy dane jako sparametryzowane wejście. Baza danych „wypełnia luki”, wstawiając przesłane dane do przygotowanej struktury. To odbiera danym specjalną moc, traktując je jako zwykłe zmienne. Przygotowane zapytania wyglądają mniej więcej tak:

    <?php
    $servername = "localhost";
    $username = "username";
    $password = "password";
    $dbname = "myDB";
    
    // Utwórz połączenie
    $conn = new mysqli($servername, $username, $password, $dbname);
    
    // Sprawdź połączenie
    if ($conn->connect_error) {
        die("Connection failed: " . $conn->connect_error);
    }
    
    // przygotuj i powiąż
    $stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
    $stmt->bind_param("sss", $firstname, $lastname, $email);
    
    // ustaw parametry i wykonaj
    $firstname = "John";
    $lastname = "Doe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Mary";
    $lastname = "Moe";
    $email = "[email protected]";
    $stmt->execute();
    
    $firstname = "Julie";
    $lastname = "Dooley";
    $email = "[email protected]";
    $stmt->execute();
    
    echo "Nowe rekordy zostały utworzone pomyślnie";
    
    $stmt->close();
    $conn->close();
    ?>

    Może się to wydawać skomplikowane, ale warto zrozumieć tę koncepcję. Tu znajdziesz dobry opis.

    Mam też wskazówkę dla tych, którzy znają rozszerzenie PHP PDO i używają go do tworzenia przygotowanych zapytań.

    Uwaga: Ostrożnie z konfiguracją PDO

    Korzystając z PDO do dostępu do bazy, możemy popaść w fałszywe poczucie bezpieczeństwa. „Skoro używam PDO, nie muszę się już niczym przejmować”. Choć PDO (lub przygotowane zapytania w MySQLi) są wystarczające do ochrony przed SQL Injection, należy uważać na konfigurację. Często kopiujemy i wklejamy kod z tutoriali, nie zastanawiając się nad tym, co ustawiamy. A pewna opcja może zniweczyć nasze wysiłki:

    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

    Ta opcja sprawia, że PDO emuluje przygotowane zapytania, zamiast korzystać z wbudowanych funkcji bazy. W rezultacie, PHP wysyła do bazy zwykłe ciągi zapytań, nawet jeśli kod wygląda jakby używał przygotowanych zapytań. Innymi słowy, nadal jesteśmy podatni na SQL Injection.

    Rozwiązanie jest proste: upewnij się, że emulacja jest wyłączona.

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

    Teraz skrypt PHP będzie zmuszony do używania przygotowanych zapytań na poziomie bazy, skutecznie chroniąc przed SQL Injection.

    Ochrona za pomocą WAF

    Czy wiesz, że aplikacje internetowe można chronić przed SQL Injection również za pomocą zapory aplikacji internetowych (WAF)?

    Nie tylko przed SQL Injection, ale też przed wieloma innymi atakami na warstwie 7, takimi jak XSS, zepsute uwierzytelnianie, CSRF, wyciek danych. Możesz użyć własnej zapory (np. Mod Security) lub zapory opartej na chmurze.

    SQL Injection w nowoczesnych frameworkach PHP

    SQL Injection to tak powszechny, prosty i niebezpieczny problem, że wszystkie nowoczesne frameworki PHP mają wbudowane mechanizmy obronne. Na przykład w WordPressie mamy funkcję $wpdb->prepare(), a frameworki MVC wykonują większość pracy za nas, chroniąc nas automatycznie przed SQL Injection. W WordPressie, przygotowanie zapytań jest jawne, ale to WordPress, więc nic nie poradzimy. 🙂

    Współcześni programiści internetowi często nie myślą o SQL Injection, przez co nie mają świadomości zagrożenia. Jeśli pozostawią otwarte tylne drzwi (np. parametr w $_GET i stare przyzwyczajenia do tworzenia „brudnych” zapytań), konsekwencje mogą być tragiczne. Dlatego warto poznać podstawy.

    Podsumowanie

    SQL Injection to nieprzyjemny atak, ale łatwo go uniknąć. Jak pokazano w artykule, wystarczy ostrożnie podchodzić do danych od użytkowników (SQL Injection to tylko jedno z zagrożeń związanych z danymi od użytkownika) i dbać o poprawność zapytań. Poza tym, nie zawsze pracujemy w bezpiecznym środowisku, więc warto być świadomym zagrożenia i nie dać się zaskoczyć.


    newsblog.pl