Dataczwartek, 13 sierpnia 2026 Czas19:01:24
← Aplikacje internetowe (PHP)

Lekcja 19. Podstawy bezpieczeństwa — SQL injection i zapytania przygotowane

Trudny / egzaminacyjny

Po co się tego uczymy?

Wszystkie dotychczasowe lekcje budowały zapytania SQL w sposób, który DZIAŁA na potrzeby nauki, ale ma realną lukę bezpieczeństwa. Ta lekcja pokazuje, jak pisać to samo profesjonalnie i bezpiecznie — umiejętność wymagana w każdej prawdziwej pracy programisty.

Teoria

Przez cały dział budowałeś zapytania SQL, "sklejając" tekst ze zmiennymi (np. "SELECT * FROM Uzytkownicy WHERE email = '$email'"). To działa, ale ma poważną słabość: jeśli użytkownik (albo ktoś złośliwy) wpisze w polu formularza fragment kodu SQL zamiast zwykłego tekstu, może to ZMIENIĆ SENS całego zapytania. To atak nazywany SQL injection.

Przykład ataku: gdyby w polu e-mail ktoś wpisał ' OR '1'='1, zapytanie logowania mogłoby się zamienić w warunek, który jest ZAWSZE prawdziwy — omijając w ten sposób sprawdzenie hasła.

Rozwiązanie: zapytania przygotowane (prepared statements). Zamiast wstawiać wartości wprost do tekstu zapytania, zostawiasz w nim "placeholder" (znak zapytania), a wartości "podpinasz" osobno — baza danych sama zadba o to, żeby dane NIGDY nie zostały potraktowane jako część polecenia SQL, tylko zawsze jako zwykłe dane.

mysqli_prepare(), bind_param() i execute() to trzy kroki tego bezpiecznego podejścia — od teraz powinieneś używać ich zamiast "sklejania" tekstu, szczególnie tam, gdzie dane pochodzą od użytkownika (czyli w praktyce: prawie zawsze).

Przykład z życia

Głośne wycieki danych z dużych serwisów internetowych bardzo często zaczynały się właśnie od SQL injection — formularza logowania czy wyszukiwania, który nie zabezpieczył się przed złośliwie spreparowanymi danymi wejściowymi.

Kod (PHP)

<?php
$polaczenie = mysqli_connect("localhost", "root", "", "serwis_filmowy");

// NIEBEZPIECZNE (nie rób tak!) - podatne na SQL injection:
// $zapytanie = "SELECT * FROM Uzytkownicy WHERE email = '$email' AND haslo = '$haslo'";

// BEZPIECZNE - zapytanie przygotowane (prepared statement)
$email = $_POST["email"];

$stmt = mysqli_prepare($polaczenie, "SELECT * FROM Uzytkownicy WHERE email = ?");
mysqli_stmt_bind_param($stmt, "s", $email); // "s" = string
mysqli_stmt_execute($stmt);
$wynik = mysqli_stmt_get_result($stmt);

if (mysqli_num_rows($wynik) === 1) {
    $uzytkownik = mysqli_fetch_assoc($wynik);
    echo "Znaleziono użytkownika: " . htmlspecialchars($uzytkownik["email"]);
} else {
    echo "Nie znaleziono takiego użytkownika.";
}

mysqli_stmt_close($stmt);
mysqli_close($polaczenie);

Komentarz i wyjaśnienie kodu

Znak zapytania (?) w zapytaniu SQL to "placeholder" — miejsce, w które trafi wartość, ale baza danych wie, że to ZAWSZE dane, nigdy fragment polecenia SQL. mysqli_stmt_bind_param($stmt, "s", $email) "podpina" zmienną $email pod ten placeholder — litera "s" oznacza, że to string (dla liczb całkowitych używa się "i", dla liczb zmiennoprzecinkowych "d"). Nawet gdyby $email zawierał złośliwy fragment SQL, zostanie potraktowany jako zwykły tekst do porównania, a nie jako część zapytania — to właśnie eliminuje ryzyko SQL injection.

Ćwiczenie samodzielne

Przepisz na zapytanie przygotowane jedno z zapytań INSERT z Lekcji 10 (dodawanie filmu) — potrzebujesz trzech placeholderów (?, ?, ?) i odpowiedniego "stringa typów" w bind_param, np. "sis" dla tekst-liczba-tekst.

Zadania do pracy własnej

  1. Przepisz zapytanie logowania z Lekcji 15 tak, żeby korzystało z prepared statements zamiast sklejania tekstu.

  2. Przepisz zapytanie wyszukiwania filmów po tytule (WHERE tytul LIKE ...) na wersję z prepared statements — zwróć uwagę, że przy LIKE placeholder musi zawierać już znaki % w podpiętej wartości, np. $fraza = "%" . $_GET["szukaj"] . "%";

  3. Przepisz CAŁY formularz rejestracji z Lekcji 14 (sprawdzenie zajętego e-maila + INSERT nowego użytkownika) tak, żeby oba zapytania SQL korzystały z prepared statements, zachowując przy tym haszowanie hasła.

Typowe błędy

Częsty błąd to pomylenie kolejności i liczby znaków w "stringu typów" w bind_param() względem liczby i kolejności placeholderów (?) w zapytaniu — muszą się dokładnie zgadzać. Drugi błąd: zapomnienie mysqli_stmt_get_result() — bez tego nie da się normalnie przetworzyć wyniku zapytania SELECT wykonanego jako prepared statement. Trzeci, najpoważniejszy: przekonanie, że skoro "kod działa" ze sklejaniem tekstu, to nie warto przechodzić na prepared statements — bezpieczeństwo nie jest widoczne w normalnym działaniu aplikacji, ujawnia się dopiero przy próbie ataku.

Nawiązanie do egzaminu zawodowego

To realizacja efektu INF.03.5.6 ("przeprowadza walidację kodu programu") w zakresie bezpieczeństwa — coraz częściej pojawia się też w informatorach i arkuszach jako świadomość zagrożeń typu SQL injection, wymieniana wprost w podstawie programowej INF.03.2 ("stosuje zasady cyberbezpieczeństwa").