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.
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'] ?? '');
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;
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?
Atak session fixation polega na narzuceniu ofierze konkretnego ID sesji przed logowaniem. Po zalogowaniu napastnik zna to ID i mógłby przejąć sesję. session_regenerate_id(true) generuje nowe ID i usuwa stare — napastnik traci dostęp.
Jak poprawnie sprawdzić czy użytkownik jest zalogowany na początku chronionej strony?
Sprawdzamy $_SESSION['user_id'] (lub inny klucz ustawiony przy logowaniu). Jeśli nie istnieje — użytkownik nie jest zalogowany. Przekierowanie przez header() musi być poprzedzone session_start() i zakończone exit — bez exit PHP kontynuuje wykonanie skryptu.
Jaka jest poprawna kolejność kroków przy wylogowaniu?
Bezpieczne wylogowanie: (1) session_start() jeśli nie wywołane wcześniej, (2) $_SESSION = [] czyści dane, (3) session_destroy() niszczy sesję po stronie serwera. Opcjonalnie usuń ciasteczko PHPSESSID.
Ć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;