Wprowadzenie do sesji w PHP
Sesja w PHP to mechanizm pozwalający zachować dane dotyczące danego użytkownika między wieloma żądaniami HTTP (stronami) w trakcie jednej wizyty na witrynie. W praktyce oznacza to, że informacje przechowywane w sesji “podróżują” wraz z użytkownikiem, dzięki czemu kolejne strony mogą korzystać z danych zapisanych wcześniej. W odróżnieniu od ciasteczek (cookies), które przechowują dane po stronie klienta (w przeglądarce użytkownika), sesje przechowują dane po stronie serwera – jest to więc bezpieczniejsze i bardziej elastyczne rozwiązanie dla danych użytkownika.
Aby to osiągnąć, PHP przy pierwszym wejściu użytkownika na stronę tworzy unikalny identyfikator sesji (ang. session ID) i zwykle przesyła go do przeglądarki w formie ciasteczka. To ciasteczko pełni rolę klucza identyfikującego – nie zawiera żadnych danych osobowych ani wrażliwych, a jedynie ową losową wartość ID. Przy każdym kolejnym żądaniu przeglądarka odsyła ten identyfikator, co pozwala skojarzyć żądanie z wcześniej zapamiętanymi na serwerze informacjami użytkownika. Jeśli aplikacja znajdzie na serwerze dane odpowiadające temu identyfikatorowi, może odtworzyć stan sesji danego użytkownika (np. informacje o jego zalogowaniu, preferencjach itp.). W ten sposób sesje umożliwiają stworzenie ciągłego, spójnego doświadczenia dla użytkownika na stronie, pomimo bezstanowego charakteru protokołu HTTP.
Do czego służą sesje? Przykładowe zastosowania sesji to m.in. utrzymanie stanu zalogowania użytkownika (tzw. user session), przechowywanie zawartości koszyka w sklepie internetowym, zachowanie danych wprowadzonych do formularza pomiędzy krokami wielostronicowego formularza, czy wyświetlanie komunikatów (np. jednorazowych komunikatów potwierdzających, tzw. flash messages). Sesje są więc przydatne wszędzie tam, gdzie potrzebne jest tymczasowe przechowanie danych na serwerze na czas trwania wizyty użytkownika na stronie.
Rozpoczęcie sesji: session_start()
i zmienna $_SESSION
Aby skorzystać z sesji w PHP, najpierw należy ją zainicjować. Służy do tego funkcja session_start()
, którą wywołujemy na samym początku skryptu PHP, przed wysłaniem jakiegokolwiek HTML czy innych danych do przeglądarki. Jeżeli żądanie HTTP nie zawiera jeszcze identyfikatora sesji (np. użytkownik odwiedza stronę po raz pierwszy), wywołanie session_start()
utworzy nową sesję i wygeneruje dla niej unikalne ID. Jeśli natomiast użytkownik posiada już aktywną sesję (np. odwiedza kolejną podstronę), session_start()
wznowi istniejącą sesję – PHP sprawdzi wtedy, czy w żądaniu przekazano identyfikator (np. w ciasteczku sesyjnym), i jeśli tak, to załaduje odpowiadające mu dane.
Po pomyślnym wywołaniu session_start()
możemy korzystać z superglobalnej tablicy $_SESSION
do zapisywania i odczytywania danych sesyjnych. Tablica $_SESSION
działa podobnie do innych tablic PHP, z tą różnicą, że jej zawartość jest utrzymywana między kolejnymi odwołaniami HTTP (dopóki trwa sesja). W praktyce każda para klucz-wartość zapisana w $_SESSION
zostanie automatycznie zachowana po stronie serwera po zakończeniu skryptu i będzie dostępna przy następnym żądaniu od tego samego użytkownika.
Przykład – rozpoczęcie sesji i ustawienie zmiennych: Poniższy kod ilustruje prosty scenariusz startu sesji i użycia zmiennych sesyjnych:
<?php
// Startujemy sesję (musi być wywołane jako pierwsze polecenie w skrypcie)
session_start();
// Ustawiamy zmienne sesyjne
$_SESSION['username'] = 'JanKowalski';
$_SESSION['role'] = 'admin';
// Odczytujemy i wykorzystujemy zmienne sesyjne
echo "Zalogowano jako: " . $_SESSION['username']; // wypisze: Zalogowano jako: JanKowalski
?>
W powyższym przykładzie wywołujemy session_start()
, aby zainicjować (lub wznowić) sesję. Następnie przy pomocy tablicy $_SESSION
zapisujemy dwie informacje: nazwę użytkownika oraz jego rolę. Te dane zostają zachowane na serwerze i będą dostępne na wszystkich kolejnych stronach, które również wywołają session_start()
na początku. Na końcu zademonstrowano odczyt zmiennej sesyjnej – wypisujemy na stronie nazwę zalogowanego użytkownika. Warto zauważyć, że zmienne sesyjne nie są przekazywane między stronami w URL czy w formularzach – PHP automatycznie utrzymuje je po stronie serwera. Wystarczy jedynie na każdej stronie, na której chcemy z nich skorzystać, ponownie wywołać session_start()
przed dostępem do $_SESSION
.
Praktyczne przykłady zastosowania sesji
Sesje najczęściej wykorzystujemy do obsługi logowania użytkowników oraz przechowywania tymczasowych danych na potrzeby aplikacji. Poniżej omówimy dwa praktyczne przykłady ilustrujące te scenariusze.
Przykład 1: Logowanie użytkownika z użyciem sesji
Rozważmy uproszczony mechanizm logowania. Gdy użytkownik poprawnie poda dane logowania (np. w formularzu), chcemy zapamiętać informację, że jest zalogowany, aby móc go uwierzytelniać na kolejnych podstronach. Do tego celu idealnie nadaje się sesja.
Załóżmy, że zweryfikowaliśmy poprawność danych logowania użytkownika (np. porównując z bazą danych). Poniższy kod demonstruje co dalej dzieje się po pomyślnym zalogowaniu:
<?php
session_start(); // Inicjujemy sesję na początku skryptu (lub kontynuujemy istniejącą)
// (Pominięto kod sprawdzający poprawność loginu i hasła użytkownika)
if ($login_ok) {
// Ustawiamy zmienne sesyjne po udanym logowaniu
$_SESSION['user_id'] = $userID; // unikalny identyfikator użytkownika (np. z bazy danych)
$_SESSION['username'] = $username; // nazwa użytkownika
$_SESSION['is_logged_in'] = true; // pomocnicza flaga stanu zalogowania
// Dla bezpieczeństwa regenerujemy ID sesji po zalogowaniu
session_regenerate_id(true);
echo "Witaj, $username! Zostałeś pomyślnie zalogowany.";
} else {
echo "Błędny login lub hasło.";
}
?>
Omówienie: Po wywołaniu session_start()
sprawdzamy dane logowania. Jeśli są poprawne, zapisujemy w sesji kluczowe informacje: identyfikator użytkownika, jego nazwę oraz ustawiamy flagę is_logged_in
informującą, że użytkownik przeszedł autoryzację. Dzięki temu na kolejnych stronach będziemy mogli łatwo sprawdzić, czy użytkownik jest zalogowany (np. poprzez if ($_SESSION['is_logged_in']) { ... }
). Następnie wywołujemy funkcję session_regenerate_id(true)
, aby zmienić identyfikator bieżącej sesji na nowy. Jest to ważna czynność zabezpieczająca przed pewnym rodzajem ataku (omówionym później jako session fixation) – po uwierzytelnieniu użytkownika warto zmienić ID jego sesji, aby ewentualny intruz nie mógł przejąć sesji, znając wcześniejsze ID. Funkcja session_regenerate_id()
generuje nowy identyfikator, zachowując przy tym zawartość bieżącej sesji (wszystkie zapisane zmienne). Argument true
powoduje, że stary identyfikator (i odpowiadający mu plik sesji) zostanie unieważniony/usunięty, zapobiegając równoległemu istnieniu dwóch sesji o tym samym stanie.
Na kolejnych stronach możemy sprawdzać, czy użytkownik jest zalogowany, np. tak:
<?php
session_start();
if (!isset($_SESSION['is_logged_in']) || $_SESSION['is_logged_in'] !== true) {
// Użytkownik nie jest zalogowany – przekierowanie lub wyświetlenie komunikatu
die("Dostęp wymaga logowania.");
}
// Jeśli warunek powyżej nie zadziałał, to znaczy, że użytkownik jest zalogowany
echo "Witaj ponownie, " . $_SESSION['username'] . "!";
?>
Dzięki sesji nie musimy za każdym razem prosić użytkownika o podanie hasła – raz zalogowany, pozostaje rozpoznawany na podstawie identyfikatora sesyjnego przesyłanego przez przeglądarkę.
Przykład 2: Przechowywanie danych tymczasowych (np. koszyk zakupów, komunikaty)
Innym powszechnym zastosowaniem sesji jest przechowywanie danych tymczasowych, które nie wymagają trwałego zapisania w bazie danych, a jedynie zachowania przez pewien czas dla wygody użytkownika. Przykładem może być koszyk zakupowy w sklepie internetowym – dopóki klient kontynuuje zakupy, lista wybranych produktów może być przechowywana właśnie w sesji (zanim zostanie ewentualnie sfinalizowana i zapisana na stałe). Innym przykładem są tzw. flash messages, czyli komunikaty (np. o błędzie lub powodzeniu operacji), które chcemy pokazać użytkownikowi tylko raz, przy kolejnym wyświetleniu strony.
Poniższy fragment kodu ilustruje mechanizm dodawania elementu do koszyka przechowywanego w sesji oraz ustawiania jednorazowego komunikatu:
<?php
session_start();
// Dodanie produktu do koszyka (przechowywanego jako tablica w sesji)
$productId = 42;
$_SESSION['cart'][] = $productId; // dodajemy ID produktu do tablicy 'cart'
$_SESSION['message'] = "Produkt $productId został dodany do koszyka."; // ustawiamy komunikat
// ... (przekierowanie do innej strony lub dalsza logika)
?>
W powyższym kodzie wykorzystujemy sesję do przechowania zawartości koszyka: zakładamy, że $_SESSION['cart']
jest tablicą, w której trzymamy identyfikatory wybranych produktów. Wywołanie $_SESSION['cart'][] = $productId;
dodaje kolejny produkt do koszyka (jeśli tablica nie istniała, PHP automatycznie ją utworzy). Dodatkowo ustawiamy zmienną $_SESSION['message']
z komunikatem informującym o dodaniu produktu – załóżmy, że chcemy wyświetlić go użytkownikowi na następnej stronie i potem usunąć. Realizacja wyświetlenia mogłaby wyglądać następująco (np. na stronie koszyka):
<?php
session_start();
if (isset($_SESSION['message'])) {
echo "<div class='msg'>" . $_SESSION['message'] . "</div>";
unset($_SESSION['message']); // usuwamy komunikat po wyświetleniu
}
// Wyświetlamy zawartość koszyka:
if (!empty($_SESSION['cart'])) {
foreach ($_SESSION['cart'] as $prodId) {
echo "Produkt o ID $prodId<br>";
}
}
?>
Dzięki sesji komunikat message
“przetrwał” przekierowanie na kolejną stronę, po czym został wypisany i skasowany. Podobnie zawartość koszyka (lista ID produktów) jest dostępna tak długo, jak trwa sesja użytkownika lub dopóki jej nie wyczyścimy.
Parametry sesji: czas życia, lokalizacja plików, identyfikator sesji
Sesja posiada pewne istotne parametry konfiguracyjne i środowiskowe, które warto znać:
- Identyfikator sesji (session ID) – to unikalny ciąg znaków, który identyfikuje sesję użytkownika. Domyślnie PHP generuje losowy 32-znakowy identyfikator w formie szesnastkowej (przykładowy identyfikator może wyglądać tak:
4af5ac6val45rf2d5vre58sd648ce5f7
). Identyfikator ten jest najczęściej przekazywany do przeglądarki w ciasteczku o nazwiePHPSESSID
(domyślna nazwa sesji w PHP). Oznacza to, że po stronie klienta sesja jest reprezentowana właśnie przez ciasteczko session ID. Alternatywnie (choć obecnie rzadziej praktykowane ze względów bezpieczeństwa), identyfikator sesji może być przekazywany w URL (tzw. trans SID) lub polu formularza – jednak domyślnie PHP od wersji 7 ma tę funkcję wyłączoną. Sam identyfikator jest wartością losową i nie zawiera żadnych informacji o użytkowniku (jest to tylko token/klucz). - Lokalizacja i przechowywanie danych sesji – standardowo PHP zapisuje dane sesyjne na serwerze w plikach. Każda nowa sesja tworzy plik, którego nazwa to zazwyczaj
sess_<identyfikator>
(np.sess_4af5ac6val45rf2d5vre58sd648ce5f7
) i który jest zapisywany w określonym katalogu na serwerze. Dzięki temu wszystkie zmienne zapisane w$_SESSION
są zachowane między wywołaniami skryptów – po wywołaniusession_start()
PHP odczytuje odpowiedni plik i odtwarza zmienne. Domyślna lokalizacja plików sesji to systemowy katalog tymczasowy (np./tmp
na serwerach Linux). Może być ona zmieniona za pomocą dyrektywy konfiguracyjnejsession.save_path
w php.ini. Ważne jest, by katalog na pliki sesji był zapisywalny przez mechanizm PHP i odpowiednio zabezpieczony – na serwerach współdzielonych nie powinien to być katalog dostępny dla innych użytkowników systemu, aby nie umożliwić podejrzenia cudzych sesji. - Czas życia sesji – po jakim czasie sesja wygaśnie? Odpowiedź nie jest jednoznaczna, ponieważ składają się na to dwa elementy:
- Czas życia ciasteczka sesji po stronie przeglądarki. Domyślnie ciasteczko sesyjne jest nietrwałe –
session.cookie_lifetime
ustawione jest na0
, co oznacza, że cookie wygaśnie z chwilą zamknięcia przeglądarkiphp.net. Można zmienić ten parametr, aby sesja utrzymywała się nawet po zamknięciu i ponownym otwarciu przeglądarki (np. ustawiającsession.cookie_lifetime
na liczbę sekund odpowiadającą 7 dniom, sesja będzie utrzymywana tydzień – często stosowane przy opcjach typu „Zapamiętaj mnie”). - Czas przechowywania danych sesji na serwerze. Nawet jeśli ciasteczko u klienta będzie ważne, może się okazać, że serwer usunie dane danej sesji po pewnym czasie nieaktywności. Parametr
session.gc_maxlifetime
określa liczbę sekund, przez jaką dane sesji na serwerze są przechowywane zanim zostaną uznane za nieużywane (ang. garbage) i mogą zostać usunięte. Innymi słowy, jest to czas, przez jaki nieużywana (nieaktywna) sesja będzie utrzymywana. Domyślnie wynosi on 1440 sekund (24 minuty). Mechanizm usuwania starych sesji realizowany jest poprzez tzw. garbage collector, który uruchamia się z pewnym prawdopodobieństwem przy okazji startu nowej sesji. W praktyce oznacza to, że jeśli użytkownik nie wyśle żadnego żądania przez okres dłuższy niżgc_maxlifetime
, jego sesja może wygasnąć (zostać skasowana po stronie serwera). Warto podkreślić, że jest różnica między wygaśnięciem sesji na skutek wyczyszczenia po stronie serwera a utratą sesji na skutek utraty cookie w przeglądarce – aby sesja była aktywna, muszą istnieć oba elementy. Z tego powodu często w aplikacjach wprowadza się dodatkowe własne mechanizmy monitorowania czasu bezczynności użytkownika (np. zapis znacznika czasu ostatniej aktywności w$_SESSION
i manualne wylogowanie po określonym okresie braku aktywności), o czym więcej w sekcji o dobrych praktykach.
- Czas życia ciasteczka sesji po stronie przeglądarki. Domyślnie ciasteczko sesyjne jest nietrwałe –
- Inne parametry – Każda sesja ma też nazwę (domyślnie wspomniane
PHPSESSID
), którą można zmienić konfigurującsession.name
– bywa to stosowane np. gdy na jednym serwerze działa wiele aplikacji PHP i chcemy unikatowej nazwy ciasteczka sesji dla każdej z nich. Istnieją również ustawienia dotyczące przekazywania identyfikatora w adresach (session.use_trans_sid
,session.use_only_cookies
), bezpieczeństwa ID (session.use_strict_mode
) i kilka innych – ich omówienie znajdzie się w dalszej części.
Zarządzanie sesjami: usuwanie danych, zamykanie sesji, zmiana ID
Podczas pracy z sesjami często potrzebujemy wykonać operacje takie jak wylogowanie użytkownika, co wiąże się z wyczyszczeniem danych sesji lub całkowitym jej zakończeniem. PHP udostępnia do tego następujące mechanizmy:
- Usuwanie danych z sesji (pojedynczych zmiennych): Możemy skasować konkretną zmienną sesyjną analogicznie jak każdą inną zmienną PHP – np.
unset($_SESSION['username']);
usunie z sesji pozycję o kluczu'username'
. Jeśli chcemy wyczyścić wszystkie zmienne sesyjne (np. podczas wylogowania), najprościej użyć funkcjisession_unset()
, która usuwa całą zawartość tablicy$_SESSION
. Alternatywnie można też ustawić$_SESSION = array();
– efekt będzie podobny (wyczyszczenie tablicy). - Zniszczenie sesji: Aby całkowicie zakończyć sesję (np. wylogować użytkownika i unieważnić jego ID sesji), używamy funkcji
session_destroy()
. Usuwa ona całą sesję po stronie serwera – skasowany zostaje plik sesji oraz identyfikator przestaje być ważny. Należy pamiętać, że przed wywołaniemsession_destroy()
dobrze jest wywołaćsession_start()
(jeśli jeszcze nie było w danym skrypcie) oraz ewentualniesession_unset()
, aby wyczyścić dane z bieżącej tablicy$_SESSION
. Przykładowy kod wylogowania może wyglądać następująco:
session_start();
session_unset(); // usuń wszystkie zmienne sesji
session_destroy(); // zakończ sesję // (opcjonalnie skasuj również cookie sesyjne u klienta, wysyłając ciasteczko z datą w przeszłości)
Po wywołaniu session_destroy()
obecna sesja przestaje istnieć – w razie kolejnego żądania od użytkownika (który może mieć jeszcze stare ciasteczko) nie zostanie ono już skojarzone z żadnymi danymi (należy utworzyć nową sesję w razie potrzeby). Innymi słowy, użytkownik został wylogowany.
- Zmiana identyfikatora sesji: Wspomniana już funkcja
session_regenerate_id()
służy do wygenerowania nowego ID dla bieżącej sesji. Mechanizm ten nie kończy sesji, a jedynie podmienia jej identyfikator na inny (dzięki czemu np. atakujący, który znał poprzedni ID, traci do niej dostęp). Dane w$_SESSION
zostają przeniesione pod nowy identyfikator, a stary może zostać usunięty (jeśli wywołano funkcję z argumentemtrue
). Zaleca się użyciesession_regenerate_id()
przede wszystkim po zalogowaniu użytkownika oraz ewentualnie okresowo w trakcie bardzo długich sesji, aby utrudnić ataki polegające na przejęciu sesji użytkownika. Uwaga: Nie należy wywoływać tej funkcji zbyt często (np. przy każdym żądaniu), aby nie obciążać serwera i nie komplikować logiki aplikacji – wystarczą kluczowe momenty, takie jak zmiana uprawnień użytkownika (logowanie, nadanie wyższych uprawnień itp.).
Podsumowując, do poprawnego zarządzania sesją należy: odpowiednio czyścić dane (usuwanie pojedynczych kluczy lub wszystkich, w zależności od potrzeb), zamykać sesję gdy nie jest już potrzebna (np. przy wylogowaniu), oraz w razie potrzeby regenerować identyfikator dla bezpieczeństwa.
Bezpieczeństwo sesji
Sesje z założenia zwiększają bezpieczeństwo (względem przechowywania danych po stronie klienta w cookies), ale same w sobie również mogą stać się celem ataków. Główne zagrożenia związane z mechanizmem sesji to przejęcie sesji przez osoby niepowołane oraz tzw. atak utrwalenia sesji. Omówimy je krótko wraz ze sposobami zabezpieczenia aplikacji.
- Session hijacking (przejęcie sesji): Ten rodzaj ataku polega na tym, że osoba atakująca stara się wykraść lub odgadnąć identyfikator sesji uprawnionego użytkownika, a następnie wykorzystać go, by uzyskać dostęp do jego konta. Innymi słowy, atakujący przejmuje token sesyjny ofiary i tym samym podszywa się pod nią. Jeśli mu się to uda, serwer nie odróżni atakującego od oryginalnego zalogowanego użytkownika, co skutkuje przejęciem jego sesji. Taki atak może być przeprowadzony na różne sposoby – np. poprzez podsłuchanie niezabezpieczonego ruchu (wykradzenie cookie sesyjnego przesyłanego czystym tekstem), poprzez atak XSS (który pozwoli odczytać cookie sesyjne skryptem w przeglądarce), atak Man-in-the-Middle itp. W efekcie atakujący uzyskuje ważny session ID i może uzyskać nieautoryzowany dostęp. Jak się zabezpieczyć? Przed tego typu atakiem należy wprowadzić szereg zabezpieczeń:
- Używaj szyfrowanego połączenia (HTTPS): Podstawową ochroną jest użycie protokołu HTTPS, tak aby komunikacja między przeglądarką a serwerem była szyfrowana. Wówczas przechwycenie identyfikatora sesji staje się znacznie trudniejsze. Włączenie flagi
session.cookie_secure
ustawia ciasteczko sesyjne tak, by było przesyłane tylko po HTTPS – dzięki temu przeglądarka nie wyśle tokenu sesji przez niezabezpieczony kanał. - Ogranicz dostęp JavaScript do cookie sesji: Ustaw flagę HttpOnly dla ciasteczka sesyjnego (
session.cookie_httponly = 1
w konfiguracji) – spowoduje to, że skrypty po stronie klienta (JavaScript) nie będą mogły odczytać wartości cookie. Chroni to przed kradzieżą ID sesji przy atakach XSS, ponieważ nawet jeśli na stronie jest luka XSS, wstrzyknięty złośliwy skrypt nie wyciągnie wartości sesji z ciasteczka (przeglądarka zablokuje taki dostęp). - Dbaj o losowość i długość identyfikatorów: PHP domyślnie generuje dość długie, kryptograficznie losowe identyfikatory sesji, co czyni ich odgadnięcie mało prawdopodobnym (spełniają one wymóg co najmniej 64 bitów entropii zgodnie z zaleceniami OWASP). Ważne, aby nie stosować własnych mechanizmów generowania ID o niższej losowości.
- Kontroluj ważność sesji w czasie: Nie powinno się pozwalać na nieograniczenie długie sesje. Warto ustawić rozsądny czas żywotności sesji (np. poprzez
gc_maxlifetime
oraz mechanizm wygaszania po stronie aplikacji) – dzięki temu nawet jeśli ID wycieknie, to po pewnym czasie stanie się bezużyteczne. Również po stronie klienta zbyt długo ważne cookies mogą stanowić ryzyko, dlatego należy rozważyć, czy na pewno potrzebujemy bardzo długiegocookie_lifetime
. Często stosuje się strategię, że po okresie bezczynności (np. 15-30 minut) sesja użytkownika jest unieważniana i musi on zalogować się ponownie. Takie podejście znacząco utrudnia wykorzystanie przechwyconych sesji. - Weryfikacja tożsamości użytkownika przy żądaniach: Można zaimplementować dodatkowe sprawdzanie, czy sesja nie została przejęta, poprzez weryfikację pewnych cech każdego żądania. Przykładowo, można zapisać w sesji adres IP oraz ciąg identyfikujący przeglądarkę (User-Agent) użytkownika w momencie logowania, a następnie przy każdym kolejnym żądaniu sprawdzać, czy te parametry się zgadzają. Jeśli nastąpi zmiana (np. inne IP w trakcie jednej sesji), może to sugerować przejęcie sesji i skutkować wylogowaniem. Ograniczeniem jest tu zmienność adresu IP u niektórych dostawców internetu – stąd mechanizm ten należy stosować rozważnie, by nie wylogowywać prawidłowych użytkowników.
- Unikaj przekazywania session ID w URL: Upewnij się, że opcja przekazywania identyfikatora sesji w adresie URL jest wyłączona (
session.use_trans_sid = 0
, co jest domyślne). ID widoczne w adresie łatwo przypadkowo ujawnić (np. w logach serwera, historii przeglądarki, poprzez przesłanie linku znajomemu itp.). Dlatego lepiej polegać wyłącznie na cookies (ustawieniesession.use_only_cookies = 1
).
- Używaj szyfrowanego połączenia (HTTPS): Podstawową ochroną jest użycie protokołu HTTPS, tak aby komunikacja między przeglądarką a serwerem była szyfrowana. Wówczas przechwycenie identyfikatora sesji staje się znacznie trudniejsze. Włączenie flagi
- Session fixation (atak utrwalenia sesji): Ten atak różni się od powyższego tym, że zamiast kraść istniejącą sesję, atakujący próbuje narzucić ofierze identyfikator sesji z góry wybrany przez siebie. Scenariusz bywa następujący: napastnik inicjuje sesję na serwerze (uzyskuje pewien session ID), a następnie nakłania ofiarę do skorzystania z tego identyfikatora – np. wysyłając jej specjalny link zawierający
PHPSESSID=...
lub poprzez wcześniejsze ustawienie cookie tego ID w przeglądarce ofiary (jeśli ma taką możliwość). Gdy ofiara zaloguje się w aplikacji mając już ten narzucony identyfikator, atakujący zna jej session ID i tym samym zyskuje dostęp do uwierzytelnionej sesji. Jak zapobiegać? Obrona przed session fixation jest stosunkowo prosta: należy zmieniać identyfikator sesji przy przejściu użytkownika do stanu uwierzytelnionego. Innymi słowy, po zalogowaniu (lub utworzeniu nowej sesji gościa) zawsze wywołujsession_regenerate_id()
– dzięki temu nawet jeśli ofiara miała narzucony przez atakującego stary identyfikator, to po zalogowaniu otrzyma nowy, którego atakujący już nie zna. W efekcie atakujący zostaje odcięty od sesji. Dodatkowo warto włączyć opcjęsession.use_strict_mode = 1
(jeśli nie jest domyślnie włączona). Strict mode sprawia, że PHP nie zaakceptuje identyfikatora sesji, który nie został wygenerowany przez niego uprzednio (np. przez wywołanie session_id() lub session_start()). Gdy przeglądarka zgłosi nieistniejący ID sesji, serwer utworzy zamiast tego nową sesję o innym ID. Ta opcja została wprowadzona jako zabezpieczenie właśnie przed atakami fixation – uniemożliwia atakującemu “przygotowanie” sesji i zmuszenie ofiary do jej przejęcia.
Podsumowując, główna zasada bezpieczeństwa to traktować identyfikator sesji jak wrażliwe dane uwierzytelniające – podobnie jak hasło. Musi on być tajny i odpowiednio chroniony, a wszelkie operacje zwiększające uprawnienia użytkownika (np. logowanie) powinny skutkować zmianą tego identyfikatora.
Ustawienia konfiguracyjne PHP wpływające na sesje
PHP posiada wiele dyrektyw konfiguracyjnych (ustawianych w php.ini lub funkcjami runtime), które determinują zachowanie mechanizmu sesji. Poniżej wymieniono najważniejsze z nich, wraz z omówieniem:
session.cookie_lifetime
– czas życia ciasteczka sesyjnego (w sekundach) wysyłanego do przeglądarki. Wartość0
oznacza, że cookie wygaśnie z chwilą zamknięcia przeglądarki (czyli jest to tzw. session cookie niewykraczające poza sesję przeglądarki). Ustawienie np.3600
sprawi, że ciasteczko będzie ważne przez 3600 sekund od momentu ustawienia (1 godzina), niezależnie od tego, czy przeglądarka jest zamknięta czy nie.session.gc_maxlifetime
– maksymalny czas życia danych sesji po stronie serwera (w sekundach). Po upływie tego czasu od ostatniego użycia sesji, dane mogą zostać usunięte przez mechanizm garbage collectora. Należy pamiętać, że jeśli różne skrypty/aplikacje współdzielą magazyn sesji, to najniższa wartośćgc_maxlifetime
spośród nich będzie decydować o usuwaniu danych (najbardziej restrykcyjny czas wygaśnięcia). Domyślnie 1440 sek (24 minuty). Uwaga: na niektórych serwerach (np. dystrybucje Linuksa Debian/Ubuntu) czyszczenie starych sesji jest realizowane przez zewnętrzny mechanizm (cron) bazujący na globalnej konfiguracji, co może sprawić, że zmianagc_maxlifetime
w .htaccess lub skrypcie nie zadziała, jeśli nie zmienimy też globalnie ustawień lub nie zastosujemy własnego mechanizmu przechowywania sesji.session.cookie_secure
– flaga określająca, czy ciasteczko sesji ma być przesyłane tylko po bezpiecznym połączeniu (HTTPS). Gdy ustawiona na1
(true), przeglądarka nie wyśle ciasteczka przez połączenie nieszyfrowane HTTP. Zaleca się włączenie tej opcji na stronach działających pod HTTPS (na stronach dostępnych po HTTP nie należy jej włączać, bo wtedy sesje w ogóle by nie działały dla użytkowników na HTTP).session.cookie_httponly
– flaga oznaczająca, że ciasteczko sesyjne ma być tylko dla protokołu HTTP. Ustawieniesession.cookie_httponly = 1
spowoduje dodanie flagi HttpOnly do ciasteczka, co zabezpiecza przed dostępem do niego przez JavaScript (mitigacja ataków XSS kradnących cookies).session.cookie_samesite
– ustawia atrybut SameSite dla cookie sesyjnego. Może przyjmować wartościLax
,Strict
alboNone
(lub być pusty/brak, co oznacza brak ustawienia SameSite). Atrybut SameSite pozwala ograniczyć wysyłanie ciasteczka w żądaniach cross-site, co stanowi ochronę przed atakami typu CSRF (Cross-Site Request Forgery).Strict
– ciasteczko nie jest wysyłane w ogóle w żądaniach do domeny z innej domeny (nawet przy kliknięciu linku).Lax
– ciasteczko nie jest wysyłane przy żądaniach typu POST z innej strony, ale wysyłane jest przy nawigacji (GET).None
– wyłącza ograniczenie (ciasteczko zawsze wysyłane, wymaga jednak ustawienia Secure). Dobrym domyślnym wyborem jestLax
lubStrict
w zależności od potrzeb aplikacji.session.save_path
– ścieżka do katalogu, gdzie przechowywane są pliki sesji (w przypadku domyślnego mechanizmu plikowego). Jeśli sesje mają być zapisywane w niestandardowym miejscu (np. osobny katalog dla aplikacji, lub NFS, czy inny współdzielony system plików między serwerami), należy tutaj ustawić odpowiednią ścieżkę. Możliwe jest również ustawienie poziomów zagnieżdżenia katalogów (opcjaN;
poprzedzająca ścieżkę) w celu lepszej organizacji dużej liczby plików – lecz w prostych zastosowaniach nie ma takiej potrzeby.session.name
– nazwa sesji, która będzie używana jako nazwa cookie. DomyślniePHPSESSID
. Można zmienić, jeśli chcemy ukryć fakt używania PHP lub mieć unikalną nazwę dla aplikacji. Należy używać tylko liter i cyfr (bez spacji, przecinków itp.) żeby cookie działało poprawnie.session.auto_start
– czy automatycznie startować sesję dla każdego requestu. Domyślnie0
(wyłączone). Gdyby ustawić na1
, sesja wystartuje automatycznie na początku każdego skryptu PHP bez potrzeby wywoływaniasession_start()
. Jednak ta opcja bywa rzadko używana, bo zmniejsza kontrolę nad tym, gdzie sesja jest tworzona; zazwyczaj lepiej samodzielnie wywoływaćsession_start()
wtedy, gdy jest to potrzebne.session.use_cookies
/session.use_only_cookies
– dotyczą mechanizmu przekazywania ID sesji.session.use_cookies
(domyślnie1
) oznacza, że PHP będzie używać ciasteczek do przekazywania identyfikatora sesji.session.use_only_cookies
(domyślnie1
) oznacza, że nie będzie doklejać identyfikatora do URL czy formularzy. Innymi słowy, wymusza korzystanie tylko z cookie. Wyłączenie tej opcji (0
) pozwalałoby na alternatywne metody transmisji ID (np. przez URL), co – jak już omawiano – nie jest bezpieczne. Zatem w normalnej sytuacji te opcje powinny pozostać włączone (zwłaszczause_only_cookies = 1
).session.use_trans_sid
– powiązana z powyższym, domyślnie0
. Gdyby była włączona, PHP automatycznie dodawałby identyfikator sesji do linków (URL) i formularzy HTML generowanych przez skrypt, gdy cookies są niedostępne. Ze względów bezpieczeństwa (i SEO) ta funkcja jest odradzana – w nowoczesnych aplikacjach zazwyczaj trzymamy się cookies. Dokumentacja ostrzega, że sesje oparte na URL niosą dodatkowe ryzyko (np. udostępnienia linku z aktywnym ID znajomym, zapisanie w zakładkach itp.).session.use_strict_mode
– jak wspomniano wcześniej, włączenie tej opcji (1
) powoduje, że PHP nie zaakceptuje identyfikatora sesji od klienta, jeśli ten identyfikator nie istnieje na serwerze (nie został wcześniej wygenerowany). Bez tego mechanizmu, gdy przeglądarka wyśle np. zgadywany lub stary ID, PHP po prostu utworzy nową sesję o takim ID (jeśli nie znalazł istniejącej) – co może zostać wykorzystane w ataku session fixation. W trybie strict, zamiast tego zostanie utworzony nowy ID. Od PHP 7.0+ zaleca się tę opcję włączyć zawsze dla bezpieczeństwa.
Powyższe ustawienia możemy zmieniać w pliku php.ini, a wiele z nich także w trakcie działania skryptu (przy użyciu ini_set()
lub specjalnych funkcji jak session_set_cookie_params()
dla ustawień cookie). Dobre zrozumienie tych parametrów pozwala dostosować sposób działania sesji do wymagań aplikacji (np. wydłużyć lub skrócić czas trwania sesji, zwiększyć bezpieczeństwo ciasteczek, zmienić magazyn sesji na inny niż plikowy itp.).
Dobre praktyki pracy z sesjami
Na koniec zebraliśmy kluczowe dobre praktyki dotyczące korzystania z sesji w PHP, podsumowując wiele z powyższych zaleceń:
- Stosuj bezpieczne ustawienia ciasteczek sesji: zawsze włączaj
session.cookie_secure
na stronach HTTPS, aby token sesji nie był przesyłany przez nieszyfrowany kanał. Włącz takżesession.cookie_httponly
, by chronić cookie przed dostępem skryptów po stronie klienta. Rozważ ustawieniesession.cookie_samesite=Lax
lubStrict
zależnie od charakteru aplikacji, by ograniczyć ryzyko CSRF. Te proste ustawienia znacząco podnoszą odporność sesji na ataki. - Regeneruj ID sesji przy ważnych wydarzeniach: zawsze wywołuj
session_regenerate_id()
po zalogowaniu użytkownika lub zmianie jego uprawnień. Dzięki temu unieważnisz ewentualne przechwycone/narzucane wcześniej identyfikatory. Możesz również regenerować ID co pewną liczbę żądań lub po upływie pewnego czasu podczas dłuższych sesji, aby ograniczyć okno czasowe, w którym skompromitowany ID byłby użyteczny. - Ogranicz czas trwania sesji i bezczynności: nie pozwalaj, by sesja trwała w nieskończoność. Implementuj mechanizm automatycznego wylogowania po okresie bezczynności użytkownika (np. 15-30 minut) – zgodnie z zaleceniami, każda sesja powinna mieć ustawiony timeout bezczynności skutkujący unieważnieniem sesji po określonym czasie braku aktywności. Dodatkowo możesz wprowadzić absolutny czas życia sesji (np. maksymalnie kilka godzin), po upływie którego wymuszone będzie ponowne zalogowanie, nawet jeśli użytkownik był aktywny. Takie zasady minimalizują ryzyko przejęcia sesji – nawet jeśli token wycieknie, atakujący ma mało czasu na jego wykorzystanie.
- Nie przechowuj w sesji informacji wrażliwych w postaci niezaszyfrowanej: unikaj trzymania w
$_SESSION
bardzo wrażliwych danych, takich jak hasła w postaci jawnej, numery kart kredytowych, czy inne poufne informacje. Sesja co prawda jest po stronie serwera, ale potencjalny atakujący, który uzyska dostęp do plików sesji (np. poprzez lukę w aplikacji lub dostęp do serwera), mógłby je odczytać. Jeśli już musisz przechować takie dane chwilowo, rozważ ich zaszyfrowanie lub inną formę ochrony. Generalnie w sesji najlepiej trzymać identyfikatory, flagi stanu, ewentualnie niewielkie mniej wrażliwe informacje – natomiast dane typu hasło czy klucz API lepiej przechowywać bezpiecznie w bazie, a w sesji trzymać jedynie odwołanie do nich (np. ID użytkownika, a nie hasło). - Waliduj sesję przy każdym żądaniu (jeśli to potrzebne): za każdym razem, gdy wykonujesz operacje wymagające uwierzytelnienia, upewniaj się, że użytkownik ma aktywną, ważną sesję i odpowiednie uprawnienia. Sprawdzaj istnienie kluczowych zmiennych sesyjnych (np. czy
$_SESSION['is_logged_in']
jest ustawione i true). Rozważ dodatkowe walidacje, jak wspomniana kontrola adresu IP czy User-Agent zapisanych w sesji względem bieżących – zwłaszcza w aplikacjach wymagających wysokiego poziomu bezpieczeństwa. Pozwoli to wykryć potencjalne przejęcia sesji w trakcie jej trwania (choć nie eliminuje wszystkich przypadków). - Zawsze poprawnie kończ sesję przy wylogowaniu: to może wydawać się oczywiste, ale warto podkreślić – zapewnij funkcjonalność wylogowania użytkownika i w jej ramach usuń wszystkie dane sesyjne oraz zniszcz sesję (
session_destroy()
). Dodatkowo możesz skasować ciasteczko sesyjne po stronie przeglądarki (wysyłając nagłówek Set-Cookie z datą wygaśnięcia w przeszłości), aby nie pozostał po nim ślad. Dzięki temu po wylogowaniu nie ma ryzyka, że ktoś “odziedziczy” starą sesję, np. korzystając z tego samego komputera. - Unikaj błędów związanych z kolejnością wysyłania danych: pamiętaj zawsze, by rozpocząć sesję przed jakimkolwiek wyjściem. Jeśli zobaczysz błąd typu “Headers already sent”, oznacza to, że wywołałeś
session_start()
za późno. Dobra praktyka to umieszczaniesession_start()
zaraz po otwarciu tagu PHP, zanim wykonany zostanie jakikolwiek kod HTML czy wyjście. To drobna kwestia, ale częsta pułapka dla początkujących.
Stosując powyższe zasady, możemy bezpiecznie i efektywnie korzystać z mechanizmu sesji w PHP. Sesje są potężnym narzędziem, które odpowiednio użyte pozwalają tworzyć dynamiczne, spersonalizowane aplikacje webowe – warto jednak zawsze pamiętać o kwestiach bezpieczeństwa i optymalnej konfiguracji. Dzięki temu sesje będą służyć nam jako niezawodny sposób przechowywania danych użytkownika w trakcie jego wizyty na stronie, bez narażania tych danych na niepotrzebne ryzyko.