Bezpieczne sesje — session_regenerate_id i ochrona tras

🎯 Po tej lekcji będziesz regenerować ID sesji po logowaniu, chronić trasy przed nieautoryzowanym dostępem i poprawnie wylogowywać użytkownika przez session_destroy.

Sesja PHP identyfikuje użytkownika między kolejnymi żądaniami HTTP. To fundament każdego systemu logowania. Ale sesja ma swoje słabości — tę lekcję poświęcamy na ich łatanie.

Session Fixation — atak i obrona

Na czym polega atak

Napastnik generuje lub zna ID sesji (np. z URL, przez XSS) i sprawia, że ofiara zaczyna używać tego konkretnego ID (np. przez link https://strona.pl/?PHPSESSID=abc123). Ofiara loguje się swoim kontem — ale napastnik zna ID sesji i może ją przejąć.

Ochrona: session_regenerate_id

Rozwiązanie jest proste — po udanym zalogowaniu wygeneruj nowe ID sesji:

<?php
declare(strict_types=1);

session_start();

// ... weryfikacja hasła przez password_verify() ...

// Hasło OK — logujemy:
session_regenerate_id(true); // true = usuń starą sesję

$_SESSION['user_id']   = $uzytkownik['id'];
$_SESSION['user_name'] = $uzytkownik['login'];

session_regenerate_id(true) — tworzy nowe ID sesji i usuwa stary plik sesji. Napastnik znający stare ID traci dostęp do sesji.

⚠️ Zawsze true: Wywołuj session_regenerate_id(true) z argumentem true. Bez niego stary plik sesji zostaje na dysku — napastnik może go nadal używać.

Kompletny przepływ logowania

<?php
declare(strict_types=1);

session_start();

$blad   = '';
$sukces = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $login  = trim($_POST['login']  ?? '');
    $haslo  = $_POST['haslo']  ?? '';

    // Pobierz użytkownika z bazy (przykład bez pełnej klasy PDO)
    // $uzytkownik = $userRepo->znajdzPoLoginie($login);

    // Symulacja:
    $uzytkownik = ['id' => 1, 'login' => $login, 'haslo_hash' => password_hash('sekret', PASSWORD_DEFAULT)];

    if ($uzytkownik !== null && password_verify($haslo, $uzytkownik['haslo_hash'])) {
        // ✅ Logowanie powiodło się
        session_regenerate_id(true); // WAŻNE: generuj nowe ID sesji

        $_SESSION['user_id']   = $uzytkownik['id'];
        $_SESSION['user_name'] = $uzytkownik['login'];

        header('Location: /panel.php');
        exit;
    }

    $blad = 'Nieprawidłowy login lub hasło.';
}
?>
<!DOCTYPE html>
<html lang="pl">
<head><meta charset="UTF-8"><title>Logowanie</title></head>
<body>
<h1>Logowanie</h1>
<?php if ($blad !== ''): ?>
    <p style="color:red"><?= htmlspecialchars($blad, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<form method="post" action="">
    <label>Login: <input type="text" name="login"></label><br>
    <label>Hasło: <input type="password" name="haslo"></label><br>
    <button type="submit">Zaloguj</button>
</form>
</body>
</html>

Ochrona tras — middleware w PHP

Każda strona dostępna tylko dla zalogowanych musi to sprawdzać. Wzorzec nazywamy ochroną trasy (route guard):

<?php
declare(strict_types=1);

session_start();

// Sprawdzenie na początku każdej chronionej strony
if (!isset($_SESSION['user_id'])) {
    header('Location: /logowanie.php');
    exit; // ZAWSZE exit po header Location!
}

// Od tego miejsca wiemy, że użytkownik jest zalogowany
$userId   = (int) $_SESSION['user_id'];
$userName = (string) ($_SESSION['user_name'] ?? '');
⚠️ exit po header Location: Samo header('Location: ...') NIE przerywa wykonania skryptu. PHP kontynuuje za nim. Bez exit chroniona treść nadal zostałaby przetworzona — to poważna luka. Zawsze dodawaj exit po przekierowaniu.

Pomocnicza funkcja ochrony

Jeśli masz wiele chronionych stron, wyciągnij sprawdzenie do funkcji:

<?php
declare(strict_types=1);

function wymagajLogowania(): void
{
    if (!isset($_SESSION['user_id'])) {
        header('Location: /logowanie.php');
        exit;
    }
}

// Użycie na każdej chronionej stronie:
session_start();
wymagajLogowania();

// Tutaj tylko zalogowani użytkownicy:
echo 'Witaj, ' . htmlspecialchars($_SESSION['user_name'] ?? '', ENT_QUOTES, 'UTF-8');

Wylogowanie

Wylogowanie to zniszczenie sesji — wszystkich danych, które PHP przechowuje po stronie serwera:

<?php
declare(strict_types=1);

session_start();

// Krok 1: Wyczyść tablicę $_SESSION
$_SESSION = [];

// Krok 2 (opcjonalnie): Usuń ciasteczko sesyjne z przeglądarki
if (isset($_COOKIE[session_name()])) {
    $params = session_get_cookie_params();
    setcookie(
        session_name(),
        '',
        time() - 3600, // dowolna przeszła chwila — przeglądarka usuwa ciasteczko
        $params['path'],
        $params['domain'],
        $params['secure'],
        $params['httponly'],
    );
}

// Krok 3: Zniszcz sesję po stronie serwera
session_destroy();

// Przekieruj do strony głównej
header('Location: /');
exit;
💡 Krótka wersja: W prostych aplikacjach wystarczą kroki 1 i 3. Usunięcie ciasteczka (krok 2) sprawia, że przeglądarka natychmiast traci ID sesji — dla bezpieczeństwa warto to dodać.

Dodatkowe praktyki bezpiecznych sesji

Limit czasu sesji

PHP nie unieważnia sesji automatycznie po np. 30 minutach bezczynności. Możesz to zaimplementować ręcznie:

<?php
declare(strict_types=1);

session_start();

$timeout = 1800; // 30 minut w sekundach

if (isset($_SESSION['ostatnia_aktywnosc']) &&
    (time() - $_SESSION['ostatnia_aktywnosc'] > $timeout)) {
    // Sesja wygasła — wyloguj
    session_destroy();
    header('Location: /logowanie.php?wygas=1');
    exit;
}

$_SESSION['ostatnia_aktywnosc'] = time();

Konfiguracja php.ini

Dobre ustawienia sesji (możesz je ustawić też przez ini_set):

session.cookie_httponly = 1     ; Ciasteczko niedostępne przez JavaScript (chroni XSS)
session.cookie_secure   = 1     ; Ciasteczko tylko przez HTTPS (produkcja)
session.cookie_samesite = Lax   ; Ochrona CSRF (Lax lub Strict)
session.use_strict_mode = 1     ; Odrzuca nieznane ID sesji

Podsumowanie

Logowanie:  session_start() → password_verify() → session_regenerate_id(true) → $_SESSION['user_id'] = ...
Ochrona:    session_start() → if (!isset($_SESSION['user_id'])) { redirect; exit; }
Wylogowanie: session_start() → $_SESSION = [] → session_destroy() → redirect

W kolejnej lekcji nauczymy się trzymać sekrety poza repozytorium — hasła do bazy, klucze API i inne poufne dane, które nie mogą trafić do GitHuba.

Funkcje w tej lekcji

Sprawdź się

Po co wywołuje się session_regenerate_id(true) zaraz po pomyślnym zalogowaniu użytkownika?

Jak poprawnie sprawdzić czy użytkownik jest zalogowany na początku chronionej strony?

Jaka jest poprawna kolejność kroków przy wylogowaniu?

Ćwiczenie

Napisz plik chroniona.php, który: (1) startuje sesję, (2) sprawdza czy istnieje $_SESSION['user_id'] — jeśli nie, przekierowuje na logowanie.php i przerywa wykonanie, (3) jeśli jest zalogowany — wyświetla 'Witaj!' i imię z $_SESSION['user_name'] przez htmlspecialchars. Następnie napisz plik wylogowanie.php, który niszczy sesję i przekierowuje na stronę główną.

Pokaż rozwiązanie
<?php
// chroniona.php
declare(strict_types=1);

session_start();

if (!isset($_SESSION['user_id'])) {
    header('Location: /logowanie.php');
    exit;
}

$imie = htmlspecialchars($_SESSION['user_name'] ?? 'Użytkowniku', ENT_QUOTES, 'UTF-8');
?>
<!DOCTYPE html>
<html lang="pl"><head><meta charset="UTF-8"><title>Strefa chroniona</title></head>
<body>
<h1>Witaj, <?= $imie ?>!</h1>
<a href="wylogowanie.php">Wyloguj</a>
</body></html>

<?php
// wylogowanie.php
declare(strict_types=1);

session_start();
$_SESSION = [];
session_destroy();
header('Location: /');
exit;