INF.03 · Aplikacje internetowe PHP

Podstawy bezpieczeństwa — SQL injection i zapytania przygotowane

Cztery granice, cztery zabezpieczenia.

  • PHP8.3
  • mysqli · MariaDB
  • Praktyka i diagnoza

Po tej lekcji: Rozpoznasz SQLi, XSS, CSRF i dostęp do cudzych rekordów oraz dobierzesz właściwą ochronę.

Zrozum mechanizm

SQL injection pojawia się, gdy dane stają się składnią zapytania. Parametry prepare/bind_param/execute oddzielają wartości od SQL. Nie parametryzują nazw tabel lub kolumn — tam stosuj zamkniętą listę. Typowanie ID jest potrzebne, lecz nie zastępuje kontroli właściciela.

XSS dotyczy interpretacji tekstu jako HTML/skryptu. H() jest właściwe dla tekstu i atrybutów HTML, nie jest uniwersalnym kodowaniem dla JavaScript, CSS czy adresów URL. CSRF dotyczy niechcianej zmiany stanu z użyciem aktywnej sesji; POST sam jej nie eliminuje, dlatego sprawdzamy losowy token przez hash_equals. IDOR dotyczy cudzych obiektów — potrzebuje autoryzacji na serwerze.

Nie ćwicz ataków na bitedu.pl, cudzych serwisach ani kontach uczniów. Próby w tej lekcji wykonujesz wyłącznie na lokalnym fikcyjnym projekcie. Wyszukaj apostrof w katalogu i upewnij się, że pozostaje danymi, zapisz tytuł zawierający znaczniki i porównaj surową wartość w bazie z bezpiecznym wyświetleniem. Do testowania użyj własnej kopii bazy.

Jak uruchamiać krótkie przykłady?

Każdy krótki przykład oznaczony własnym plikiem uruchamiaj osobno, nie doklejaj wszystkich definicji do jednego pliku. W edytorze zapisz go jako .php, a w terminalu tego folderu wpisz np. php for.php. Wtedy pokazane końce wierszy PHP_EOL są widoczne. W odpowiedzi HTML znak końca wiersza sam nie robi nowej linii na ekranie: użyj <br> albo znacznika <pre>. Wyświetl źródło pozwala zobaczyć tekst odpowiedzi.

Przykłady opisane jako fragment projektu wymagają paczki KINO / LAB i wcześniejszej konfiguracji. Nie uruchamiaj ich jako samodzielnych endpointów. Pełny plik niżej pokazuje kontekst. Funkcje h(), pole() i wymagajPost() są nasze; ich definicje wczytuje bootstrap.php.

Kodowanie HTML i parametry SQL mają różne zadania

Ten sam tytuł może być zwykłym tekstem dla bazy, ale znacznikiem dla HTML. Zabezpieczenie dobierasz do miejsca użycia danych.

Podstawowa forma: htmlspecialchars(tekst, flagi, kodowanie)

escape.phpphp
<?php
$tytul = '<b>Ocean</b>';
echo htmlspecialchars($tytul, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
Wynik
&lt;b&gt;Ocean&lt;/b&gt;
  1. Pierwszy argument jest tekstem. ENT_QUOTES obejmuje oba rodzaje cudzysłowów, ENT_SUBSTITUTE zastępuje niepoprawne sekwencje kodowania. | łączy flagi.
  2. W źródle odpowiedzi pojawi się &lt;b&gt;, więc przeglądarka wyświetli dosłowne <b>Ocean</b>, a nie pogrubienie.
  3. h() w projekcie skraca właśnie to wywołanie. Nie używaj h jako zamiennika bind_param — HTML i SQL mają inne reguły.

Token formularza: wygeneruj, wyślij, porównaj

Token CSRF jest losową wartością przypisaną do sesji. Serwer dodaje ją do formularza i sprawdza po otrzymaniu POST. To nie hasło, ID konta ani klucz zamówienia.

Podstawowa forma: hash_equals(token_z_sesji, token_z_POST)

csrf.phpphp
<?php
// Demonstracja porównania na fikcyjnych tekstach:
$tokenSesji = 'przykladowy-token';
$tokenFormularza = 'inny-token';
if (!hash_equals($tokenSesji, $tokenFormularza)) {
    echo 'Odmowa: token nie pasuje';
}
Wynik
Odmowa: token nie pasuje
  1. hash_equals porównuje dwa napisy w sposób przeznaczony do bezpiecznego porównania sekretów. ! odwraca wynik: odmawiamy, gdy nie pasują.
  2. Prawdziwy token w bootstrap powstaje przez bin2hex(random_bytes(32)): 32 losowe bajty zamieniamy na 64 znaki zapisu szesnastkowego. Nie używaj stałego tekstu z demonstracji.
  3. tokenFormularza() tworzy input hidden, a wymagajPost() odczytuje wartość i ją porównuje. Ukryte pole nie znaczy tajne — można je obejrzeć w HTML.

Jak czytać pozostałe zapisy w pełnym przykładzie

ZapisZnaczeniePrzykład użycia
pole($dane, $nazwa)Nasza funkcja: pobiera wskazany klucz tylko wtedy, gdy ma wartość string; inaczej oddaje pusty napis. isset sprawdza obecność i brak null, is_string typ.pole($_POST, "email")
h($tekst)Nasza funkcja kodująca tekst/atrybut HTML przez htmlspecialchars; nie wykonuje walidacji zakresu ani zabezpieczenia SQL.h($film["tytul"])
wymagajPost()Nasza funkcja: sprawdza metodę POST oraz CSRF; przy błędzie przerywa odpowiedź z405/403.wymagajPost();
wymagajLogowania()Nasza funkcja: oddaje int ID konta z sesji lub przekierowuje do logowania. $id = wymagajLogowania();
tokenFormularza()Nasza funkcja zwraca HTML ukrytego pola csrf z wartością sesji zakodowaną przez h.<?= tokenFormularza() ?>
przekieruj($adres)Nasza funkcja wysyła Location ze statusem303 i kończy wykonanie przez exit. Typ wyniku never oznacza, że nie wraca do dalszej instrukcji.przekieruj("/panel.php");
pieniadze($grosze)Nasza funkcja formatuje grosze jako złote. number_format przyjmuje liczbę, liczbę miejsc po przecinku, separator dziesiętny i separator grup. Wynik to string.number_format(12.99, 2, ",", " ")
$_SESSION i $_POSTSpecjalne tablice PHP: pierwsza to stan sesji, druga dane formularza POST. Zewnętrzne wartości trzeba sprawdzić, nie ufać samym nazwom kluczy.$_SESSION["user_id"]
===, !==, &&, ||, !Identyczne, nieidentyczne, i, lub, zaprzeczenie. Porównanie ścisłe obejmuje typ; &&/|| nie sprawdzają drugiej części, gdy wynik jest już rozstrzygnięty.$rok === false || $rok < 1888
(int), : int, : void, : never(int) konwertuje wartość. Po dwukropku w definicji funkcji deklarujesz typ wyniku, nie wykonujesz konwersji: int liczba, void brak wyniku, never brak powrotu.function id(): int { return 7; }

Sprawdź w działaniu

Cztery granice, cztery zabezpieczenia — sprawdź w modelu

Model w przeglądarce wyjaśnia mechanizm na fikcyjnych danych. Nie wykonuje PHP ani zapytań SQL i nie tworzy kont. Prawdziwe skrypty uruchomisz lokalnie z paczki.

Ta interakcja wymaga włączonego JavaScriptu.

Przewidź rezultat, wykonaj próbę i wyjaśnij, która decyzja po stronie serwera decyduje o wyniku.

Uruchom krok po kroku

Prześledź w paczce cztery miejsca: przygotowane SELECT, h() w index.php, wymagajPost() z tokenem oraz WHERE id=? AND wlasciciel_id=? w CRUD. Do każdej próby zapisz oczekiwany status i brak niepożądanej zmiany w bazie. Nie usuwaj zabezpieczeń w działającej szkolnej platformie.

Funkcje ochrony i odpowiedzi

src/funkcje.phpphp
<?php

declare(strict_types=1);

function h(string $tekst): string
{
    return htmlspecialchars($tekst, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function pole(array $dane, string $nazwa): string
{
    // email[]=x jest tablicą, nie poprawnym tekstem. Nie rzutuj jej na string.
    return isset($dane[$nazwa]) && is_string($dane[$nazwa]) ? $dane[$nazwa] : '';
}

function przekieruj(string $adres): never
{
    header('Location: '.$adres, true, 303);
    exit;
}

function wymagajPost(): void
{
    if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
        header('Allow: POST');
        http_response_code(405);
        exit('Ta operacja wymaga POST.');
    }
    $token = pole($_POST, 'csrf');
    if ($token === '' || ! hash_equals($_SESSION['csrf'], $token)) {
        http_response_code(403);
        exit('Nieprawidłowy token formularza. Odśwież stronę.');
    }
}

function wymagajLogowania(): int
{
    if (! isset($_SESSION['user_id'])) {
        przekieruj('/logowanie.php');
    }

    return (int) $_SESSION['user_id'];
}

function tokenFormularza(): string
{
    return '<input type="hidden" name="csrf" value="'.h($_SESSION['csrf']).'">';
}

function pieniadze(int $grosze): string
{
    return number_format($grosze / 100, 2, ',', ' ').' zł';
}

Pobierz i odtwórz projekt

W „Źródłach i materiałach” na końcu pobierz KINO / LAB.zip. Rozpakuj całość; instrukcja START.txt opisuje konfigurację. Pierwsze 6 przykładów w public/podstawy działa bez bazy. Dalsze wymagają lokalnej MariaDB 11.4, importu schema.sql i lokalnego konta bazy z minimalnymi prawami. Gotowa paczka pokazuje całość, ale w tej lekcji wykonaj opisane kroki i własne zadanie.

Ćwiczenie z pomocą · samodzielna praca

Twoja próba — podpowiedzi otwieraj po kolei

Przygotuj 4 próby lokalne: apostrof w wyszukiwaniu, HTML w tytule, POST bez CSRF, cudzy ID.

Oczekiwany rezultat
Brak błęduSQL; HTML jako tekst;403 bez tokenu;404 dla cudzego rekordu.
Potrzebujesz pomocy? Rozwiązuj krok po kroku.
Krok 1 — pokaż podpowiedź

Odszukaj wskazany plik i zapisz przewidywany rezultat przed zmianą.

Krok 2 — pokaż podpowiedź

Zmień jedną rzecz. Sprawdź metodę żądania, dane i odpowiedź lub wynik programu.

Krok 3 — pokaż podpowiedź

Wykonaj próbę graniczną lub błędną. Porównaj także stan bazy lub sesji, nie tylko komunikat.

Otwórz tylko tyle podpowiedzi, ile potrzebujesz. Po każdym kroku wróć do zadania i sprawdź wynik.

Podsumowanie

Co warto zapamiętać

  1. Rozpoznasz SQLi, XSS, CSRF i dostęp do cudzych rekordów oraz dobierzesz właściwą ochronę.
  2. Potrafisz wskazać w kodzie przyczynę wyniku i zaplanować próbę błędnych danych.
  3. Nie oddajesz haseł, pliku config.local.php ani wartości cookie.

Dalej na swoim koncie

Rozgrzewki, quiz i zadania są w biurku ucznia.

Teoria jest otwarta dla wszystkich. Żeby rozwiązywać zadania, zapisywać szkice i widzieć swój postęp, zaloguj się.

Zaloguj się →
Źródła i dalsza nauka