OWASP Top 10: разбор уязвимостей с примерами на PHP (редакция 2025)
Безопасность веб-приложений — это не роскошь, а необходимость. Сайты ежедневно подвергаются атакам: SQL-инъекции, XSS, подделка запросов, утечки данных, атаки через цепочку поставок. По данным отчёта Verizon DBIR, атаки на веб-приложения стабильно входят в число ведущих причин утечек, и большинство из них предотвратимы — если знать, где именно ломаются границы доверия.
OWASP Top 10 — список десяти самых критичных рисков безопасности веб-приложений, который выпускает фонд OWASP (Open Worldwide Application Security Project). Это не догма, а ориентир для аудита и защиты.
Что обновилось. В этой статье разобран актуальный список OWASP Top 10:2025 — восьмая редакция, официально заменившая редакцию 2021 года. В ней две новые категории, одна слияние, переставлены места и пересмотрены названия. Ниже — каждая уязвимость: суть, уязвимый код на PHP, как её эксплуатируют и как закрыть.
Важно: все примеры приведены исключительно в образовательных целях. Не используйте их для атак.
Что изменилось в редакции 2025 (коротко)
- Добавлены
A03:2025 — Software Supply Chain Failures(выросла из бывших «устаревших компонентов» в отдельную большую тему про всю цепочку поставок) иA10:2025 — Mishandling of Exceptional Conditions(некорректная обработка ошибок и нештатных ситуаций, fail-open). - SSRF больше не отдельная строка Top-10. Риск «встроен» в
A01 Broken Access Control(доступ к внутренним ресурсам). Как техника он не стал менее важен — поэтому ему посвящён отдельный разбор в конце статьи. - Переставлены места: Security Misconfiguration поднялась с 5-го на 2-е; Cryptographic Failures и Injection опустились на две позиции (на 4-е и 5-е).
- Переименованы категории: «Identification and Authentication Failures» → Authentication Failures; «Vulnerable and Outdated Components» → Software Supply Chain Failures; «Logging and Monitoring Failures» → Logging & Alerting Failures; «Software and Data Integrity Failures» → «Software or Data Integrity Failures».
- Стал шире охват: анализировалось 589 CWE (против ~400 в 2021), в сами категории вошли 248 CWE; категория A01 включает до 40 CWE, A03 и A09 — по 5.
Соответствие старой и новой нумерации:
| Редакция 2021 | Редакция 2025 |
|---|---|
| A01 Broken Access Control | A01 Broken Access Control |
| A02 Cryptographic Failures | A04 Cryptographic Failures |
| A03 Injection | A05 Injection |
| A04 Insecure Design | A06 Insecure Design |
| A05 Security Misconfiguration | A02 Security Misconfiguration |
| A06 Vulnerable and Outdated Components | A03 Software Supply Chain Failures |
| A07 Identification and Authentication Failures | A07 Authentication Failures |
| A08 Software and Data Integrity Failures | A08 Software or Data Integrity Failures |
| A09 Security Logging and Monitoring Failures | A09 Security Logging & Alerting Failures |
| A10 Server-Side Request Forgery (SSRF) | A10 Mishandling of Exceptional Conditions (SSRF → отдельный разбор) |
A01:2025 — Broken Access Control (нарушение контроля доступа)
Суть: пользователь получает доступ к данным или функциям, которые ему не разрешены — по горизонтали (чужие данные) или по вертикали (чужие роли). Самая распространённая категория Top-10; в неё же теперь входит и SSRF (доступ к внутренним ресурсам через сервер).
Уязвимый PHP:
// profile.php — просмотр профиля по ID
$userId = $_GET['id'];
$user = getUserById($userId);
echo "Имя: " . $user['name'];
echo "Email: " . $user['email'];Если не проверять, что $userId принадлежит текущему пользователю, любой подменит ?id=2 и увидит чужой профиль.
Эксплуатация: перебор ID (IDOR), подмена параметров (?admin=1), прямой доступ к «скрытым» URL, обход проверки на клиенте.
Защита: проверяйте права на сервере на каждом запросе; идентифицируйте пользователя по сессии, а не по параметрам из URL.
session_start();
$currentUserId = (int) ($_SESSION['user_id'] ?? 0);
$requestedId = (int) ($_GET['id'] ?? 0); // приводим к int — против type-juggling
if ($currentUserId === 0) { // fail closed: нет сессии — нет доступа
http_response_code(401);
exit('Требуется вход');
}
if ($currentUserId !== $requestedId && !isAdmin($currentUserId)) {
http_response_code(403);
exit('Доступ запрещён');
}A02:2025 — Security Misconfiguration (небезопасная конфигурация)
Суть: неверные настройки сервера, приложения и БД: открытые порты, дефолтные пароли, включённый debug на проде, лишние сервисы, устаревшие TLS. В 2025 поднялась на 2-е место — поведение приложений всё сильнее зависит от конфигурации.
Пример: вывод ошибок PHP на проде.
; php.ini на продакшене — ТАК НЕЛЬЗЯ
display_errors = OnЭто раскрывает пути к файлам, структуру БД и версии ПО.
Защита:
; продакшен
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log- Удалите стандартные аккаунты и демо-контент, закройте лишние порты.
- Отключите неиспользуемые модули и директории листинга.
- Настройте заголовки безопасности (CSP, HSTS,
X-Content-Type-Options). - Автоматизируйте проверку конфигураций (сканеры, IaC-линтеры).
- Обновляйте ПО (см. также A03).
A03:2025 — Software Supply Chain Failures (сбои цепочки поставок ПО)
Суть: компрометация не только «своего» кода, а всей экосистемы зависимостей, сборки и дистрибуции: заражённые пакеты, подменённые реестры, уязвимые транзитивные зависимости, атаки на CI/CD, typosquatting, перехват обновлений и подписанных артефактов. В сообществе это назвали риском №1 по важности, хотя в автоматических тестах он пока мало представлен.
Примеры рисков:
- старый WordPress/PHP/jQuery с известными CVE;
- пакет из npm/Composer с подменённым maintainer’ом или удалённый и «перехваченный» под тем же именем;
- сборка, подтягивающая
latestвместо зафиксированной версии; - артефакт из CI/CD без проверки подписи.
Защита:
# PHP/Composer — аудит зависимостей на известные CVE
composer audit
# Node — аналогично
npm audit- Фиксируйте версии и lock-файлы (
composer.lock,package-lock.json), никогда не тянитеlatestна проде. - Обновляйте зависимости регулярно и удаляйте неиспользуемые.
- Следите за CVE и подписками безопасности своих пакетов.
- Проверяйте целостность и происхождение артефактов: контрольные суммы, подписи (cosign/Sigstore), подписанные коммиты в Git, воспроизводимые сборки.
- Ограничьте цепочку сборки: изолированные раннеры CI/CD, право публиковать — у минимум аккаунтов, 2FA для реестров.
- Проверяйте имя пакета по буквам (защита от typosquatting) и используйте приватные зеркала/прокси реестров.
A04:2025 — Cryptographic Failures (криптографические сбои)
Суть: неверное шифрование, хранение паролей в открытом виде, передача по HTTP, слабые алгоритмы (MD5/SHA1). Раньше эта категория называлась Sensitive Data Exposure; акцент смещён на корневую причину — плохую криптографию.
Уязвимый PHP:
// Хранение пароля в открытом виде + SQL-конкатенация (сразу и A05)
$password = $_POST['password'];
$query = "INSERT INTO users (password) VALUES ('$password')";Эксплуатация: утечка БД отдаёт все пароли; перехват трафика по HTTP; подбор по хешу, если это MD5/SHA1 без соли.
Защита:
// Хеш с адаптивной стоимостью (bcrypt/Argon2 — выбирает PHP сам)
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
// Проверка
if (password_verify($inputPassword, $storedHash)) {
// успешный вход
}- Используйте
password_hash()иpassword_verify(), а не самодельные хеши. - Всегда HTTPS; шифрование данных при передаче и при хранении.
- Не изобретайте свои алгоритмы; берите проверенные библиотеки и актуальные параметры (AES-GCM, Argon2id и т. п.).
A05:2025 — Injection (инъекции)
Суть: внедрение вредоносного кода через пользовательский ввод: SQL, команды ОС, LDAP, XPath и другие. Самая «тестируемая» категория — от частого XSS до редкой, но тяжёлой SQL-инъекции.
Уязвимый PHP:
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);Если ввести admin' --, запрос «обрезается» комментарием:
SELECT * FROM users WHERE username = 'admin' --' AND password = '...'И вход проходит без пароля. (Канонический payload именно с комментарием — он не зависит от приоритета AND/OR.)
Защита — параметризованные запросы. Хеш пароля сверяем в PHP, а не в SQL (пароли уже хешированы — см. A04):
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE username = ? LIMIT 1");
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// Сравнение выполняем ВСЕГДА — даже если пользователя нет (против timing-разведки «логин существует?»)
$hash = $user['password_hash']
?? '$2y$10$C6UzMDM.H6dfI/f/IKcEeO7ZBpBGVpZ3mB1t6p7dOz1k7mYyGQxG'; // dummy-хеш
if ($user && password_verify($password, $hash)) {
session_regenerate_id(true); // фиксируем новую сессию после входа (см. A07)
$_SESSION['user_id'] = (int) $user['id'];
} else {
// Одно сообщение и для «нет такого логина», и для «неверный пароль»
throw new RuntimeException('Неверный логин или пароль');
}Для командных инъекций оборачивайте аргументы в escapeshellarg() (а не весь запрос через escapeshellcmd()):
$file = $_GET['file'] ?? '';
$cmd = 'convert ' . escapeshellarg($file) . ' ' . escapeshellarg('out.png');
exec($cmd, $output, $exitCode); // ; и | внутрь кавычек не пролезают- Никогда не подставляйте ввод прямо в запрос — только prepared statements / ORM.
- Применяйте контекстную экранировку вывода (XSS — часть этой категории).
- Меньше всего — фильтрация «по чёрному списку»: она обходится.
A06:2025 — Insecure Design (небезопасный проект)
Суть: архитектурные ошибки, которые нельзя залатать точечным фиксом: нет лимита попыток входа, сброс пароля без подтверждения, небезопасные бизнес-процессы. В 2025 сместилась на 6-е место.
Пример: восстановление пароля, которое просто меняет пароль по email — без токена.
// ТАК НЕЛЬЗЯ: любой, кто знает чужой email, сменит пароль
$email = $_POST['email'];
$newPassword = $_POST['new_password'];
$query = "UPDATE users SET password = '$newPassword' WHERE email = '$email'";Защита:
- Продумывайте безопасность на этапе проектирования (threat modeling).
- Сброс пароля — через одноразовый подписанный токен с коротким TTL, привязанный к сессии, а не «по email в форме».
- Ограничивайте количество попыток; внедряйте MFA.
- Проектируйте по принципу наименьших привилегий и отказ по умолчанию (fail closed).
A07:2025 — Authentication Failures (сбои аутентификации)
Суть: слабая аутентификация: простые пароли, нет блокировки после неудачных попыток, небезопасные сессии, кража учётных данных. Название короче прежнего («Identification and …» убрано), смысл тот же.
Пример: сессия в cookie без флагов HttpOnly и Secure.
session_start(); // уязвимо к XSS-краже cookie и перехвату по HTTPЗащита:
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true, // только по HTTPS
'httponly' => true, // недоступно из JS — против кражи через XSS
'samesite' => 'Lax', // 'Strict'/'Lax' — против CSRF
]);
session_start();
session_regenerate_id(true); // новый ID после входа — против session fixation- Требуйте стойкие пароли и сверяйте их с базами утёкших.
- Ограничивайте попытки входа (rate limiting) и логируйте неудачи (см. A09).
- Внедряйте 2FA; не храните «запомненные» токены в открытом виде.
A08:2025 — Software or Data Integrity Failures (нарушения целостности ПО и данных)
Суть: непроверенные обновления и зависимости, небезопасная десериализация, отсутствие проверки подписей — на более низком уровне, чем вся цепочка поставок (A03).
Пример: десериализация недоверенных данных.
$data = $_COOKIE['user_data'];
$user = unserialize($data); // объект атакующего может выполнить код (POP-цепочки)Защита:
- Не десериализуйте пользовательские данные. Вместо
serialize()/unserialize()— JSON:$user = json_decode($_COOKIE['user_data'], true, 512, JSON_THROW_ON_ERROR); // ассоц. массив, не объект - Если без сериализации не обойтись — разрешайте конкретные классы:
unserialize($data, ['allowed_classes' => [User::class]]). - Проверяйте целостность обновлений (контрольные суммы, подписи) и используйте подписанные коммиты/артефакты.
A09:2025 — Security Logging & Alerting Failures (логирование и оповещения)
Суть: нет логов важных событий и, главное, нет оповещений — атаку не замечают вовремя. Прежнее «Monitoring» переименовали в «Alerting»: хороший лог без реакции почти бесполезен.
Пример проблемы: запись ввода в лог сырым — это CRLF/лог-инъекция, строку лога можно подделать:
// ТАК НЕЛЬЗЯ: $username может содержать \r\n и «нарисовать» поддельную строку лога
error_log("Неудачная попытка входа: " . $username . " с IP " . $_SERVER['REMOTE_ADDR']);Защита — санитизация лога:
function log_login_failure(string $username, string $ip): void {
// одна строка = одна запись: режем управляющие символы (CR/LF и проч.)
$safeUser = preg_replace('/[\x00-\x1F\x7F]/', '', $username);
$safeIp = filter_var($ip, FILTER_VALIDATE_IP) ? $ip : 'unknown';
error_log(sprintf('LOGIN_FAILED user="%s" ip=%s at=%s', $safeUser, $safeIp, date('c')));
}
log_login_failure($_POST['username'] ?? '', $_SERVER['REMOTE_ADDR'] ?? '');И оповещение — не ждём ручного разбора логов (в WordPress вместо сессии используйте get_transient()/set_transient()):
session_start();
$key = 'failed_login_' . ($_SERVER['REMOTE_ADDR'] ?? 'na');
$count = (int) ($_SESSION[$key] ?? 0) + 1;
$_SESSION[$key] = $count;
if ($count === 10) {
@mail(getenv('SECURITY_ALERT_EMAIL') ?: 'admin@example.com',
'Возможный брутфорс',
"10 неудачных входов с {$_SERVER['REMOTE_ADDR']}");
}- Логируйте входы/выходы, сбои аутентификации, контроль доступа и финансовые события.
- Храните логи в целостном, защищённом, централизованном виде (без PII и секретов внутри).
- Настраивайте алерты и реакция на аномалии; регулярно пересматривайте правила.
A10:2025 — Mishandling of Exceptional Conditions (нештатные ситуации)
Новая категория 2025. Суть: приложение неверно обрабатывает ошибки и необычные условия — отдаёт стек-трейс наружу, «отказывает открыто» (при сбое доступ разрешается), глотает security-исключения пустым catch, выводит display_errors на проде.
Плохо — сырая ошибка наружу:
try {
$row = $pdo->query("SELECT * FROM orders WHERE id = $id")->fetch();
} catch (Throwable $e) {
echo $e->getMessage(); // ← пути к файлам, SQL и версии ПО уходят клиенту
}Хорошо — подробно логируем, наружу нейтрально (fail closed):
try {
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([(int) $id]);
$row = $stmt->fetch();
} catch (PDOException $e) {
error_log(sprintf('[%s] %s in %s:%d', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()));
http_response_code(500);
exit('Произошла ошибка. Повторите позже.'); // без деталей для клиента
}Отдельный класс ошибки — «fail open» при сбое: пустой catch или дефолт на ошибку выдаёт лишние права:
// ПЛОХО: сбой БД = пользователь получил «admin»
$roles = @fetchRoles($user) ?: ['admin'];
// ХОРОШО: сбой = отказ (пусто), дальше проверка прав сама запретит доступ
try {
$roles = fetchRoles($user);
} catch (Throwable $e) {
error_log('role fetch failed: ' . $e->getMessage());
$roles = [];
}Защита:
- Обрабатывайте исключения осознанно, никогда не «проглатывайте» их молча.
- Проектируйте отказ по умолчанию (fail closed): при сбое — запрет, а не допуск.
- На проде
display_errors = Off,log_errors = On— стек не должен дублироваться в HTML (связь с A02). - Возвращайте клиенту обезличенные сообщения, подробности — только в логи.
Бонус: SSRF — почему его убрали из Top-10 и как защищаться
Server-Side Request Forgery в 2025 не занимает отдельную строку — риск включён в A01 (несанкционированный доступ к внутренним ресурсам). Но техника осталась крайне частой, поэтому — отдельный разбор.
Пример: загрузка изображения по пользовательскому URL.
// ТАК НЕЛЬЗЯ
$url = $_GET['url'];
$image = file_get_contents($url);
file_put_contents('/tmp/image.jpg', $image);Злоумышленник укажет http://localhost/admin или http://169.254.169.254/latest/meta-data/ (метаданные облака) и получит доступ к внутренним сервисам.
Почему «проверка хоста по белому списку» недостаточна. Типичный сниппет защищается так:
$parsed = parse_url($url);
if (!in_array($parsed['host'], ['example.com'])) { exit('Запрещённый хост'); }Но это обходится: редирект с разрешённого хоста (Location: http://169.254.169.254/) уходит «внутрь», IP-литерал и DNS-rebinding дают внутренний адрес под «безопасным» именем, а file_get_contents по умолчанию следует редиректам. Нужна проверка реально разрешённого адреса и запрет редиректов.
Корректная защита:
function safe_remote_url_check(string $url, array $allowedHosts): bool {
$p = parse_url($url);
if (!$p || empty($p['scheme']) || empty($p['host'])) return false;
if (!in_array(strtolower($p['scheme']), ['http', 'https'], true)) return false; // не file://, gopher://
$host = strtolower($p['host']);
if (!in_array($host, $allowedHosts, true)) return false; // жёсткий allow-list имён
// Резолвим имя и отсекаем приватные/зарезервированные адреса:
// localhost, 169.254.169.254 (метаданные), 10/8, 127/8, ::1 и DNS-rebinding.
$ips = [];
foreach (@dns_get_record($host, DNS_A | DNS_AAAA) ?: [] as $r) {
if (!empty($r['ip'])) $ips[] = $r['ip'];
if (!empty($r['ipv6'])) $ips[] = $r['ipv6'];
}
if (!$ips && ($ip = gethostbyname($host)) !== $host) $ips[] = $ip;
if (!$ips) return false; // имя не разрешилось — отказ
foreach ($ips as $ip) {
$public = filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)
?: filter_var($ip, FILTER_VALIDATE_IP,
FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if (!$public) return false; // хоть один адрес приватный — отказ
}
return true;
}
$url = $_GET['url'] ?? '';
if (!safe_remote_url_check($url, ['example.com', 'cdn.example.com'])) {
http_response_code(400);
exit('Разрешённый источник недоступен');
}
// Загрузка: без слепого следования редиректам, с таймаутом и лимитом размера
$ctx = stream_context_create(['http' => [
'follow_location' => 0, // редирект на внутренний хост не пройдёт повторную проверку
'timeout' => 5,
'max_redirects' => 0,
]]);
$image = @file_get_contents($url, false, $ctx, 0, 5 * 1024 * 1024); // ≤ 5 МБ
if ($image === false || strlen($image) === 0) {
http_response_code(502);
exit('Не удалось получить файл');
}
// Имя файла НЕ берём от пользователя — генерируем своё
$saved = '/tmp/' . bin2hex(random_bytes(8)) . '.img';
file_put_contents($saved, $image);WordPress-альтернатива одной строкой (делает часть guard’ов сама — валидирует схему, отсекает приватные IP, блокирует внешние хосты):
wp_safe_remote_get($url, ['timeout' => 5, 'redirection' => 0])при включённыхWP_HTTP_BLOCK_EXTERNALи allow-list вWP_ACCESSIBLE_HOSTS.
Актуальные версии PHP (на октябрь 2026)
«Устаревшие компоненты» из A03/A06 — в первую очередь про сам интерпретатор. Держите PHP в поддерживаемой ветке:
| Ветка | Статус (окт. 2026) |
|---|---|
| PHP 8.5 | актуальная (релиз ноябрь 2025) — лучший выбор для нового |
| PHP 8.4 | активная поддержка до конца 2026, безопасности — до конца 2028 |
| PHP 8.3 | только безопасность до конца 2027 |
| PHP 8.2 | безопасность заканчивается 31.12.2026 — пора уходить |
| PHP 8.1 и ниже | EOL — без обновлений безопасности |
Минимум для продакшена сегодня — 8.3, оптимально — 8.4/8.5. Цикл поддержки PHP: 2 года активных исправлений + 2 года критических правок безопасности.
Чек-лист для самопроверки
- Права доступа проверяются на сервере на каждом запросе; ввод приведён к нужному типу (A01).
- Нет внешних редиректов к внутренним адресам; исходящие запросы — по allow-list с проверкой реального IP (A01/SSRF).
- Конфигурация проде:
display_errors = Off, дефолтные аккаунты удалены, лишние порты закрыты, заголовки безопасности проставлены (A02). - Зависимости зафиксированы (lock-файлы), проходит
composer audit/npm audit, артефакты проверяются на подпись (A03). - Пароли — только
password_hash()/password_verify(); HTTPS везде (A04). - Все обращения к БД — prepared statements; вывод контекстно экранируется; аргументы команд — через
escapeshellarg()(A05). - Безопасность продумана на этапе дизайна: токены сброса, лимит попыток, fail closed (A06).
- Сессии защищены (
HttpOnly,Secure,SameSite), ID регенерируется после входа, есть MFA (A07). - Нет
unserialize()на пользовательских данных; целостность обновлений проверяется (A08). - Логируются security-события, ввод в лог санитизирован, настроены оповещения (A09).
- Исключения обрабатываются осознанно, наружу уходят обезличенные сообщения, при сбое — отказ, а не допуск (A10).
Инструмент под задачу
Разбор .htaccess/nginx-правил на предмет «что реально блокирует, а что создаёт иллюзию защиты» — по тем же A05/A10, что прошли выше. Попробуйте инструмент прямо здесь, на своих данных:
ИИ найдёт уязвимости и предложит исправления.
Источники
- OWASP Top 10:2025 — Release и Introduction: https://top10.owasp.org/2025/
- Проект OWASP Top 10 (переход на редакцию 2025): https://owasp.org/projects/top-ten
- Статус поддержки версий PHP: https://endoflife.date/php и https://www.php.net/supported-versions.php
- Verizon DBIR — тенденции атак на веб-приложения (2025): https://www.verizon.com/business/resources/reports/2025-dbir-data-breach-investigations-report.pdf
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
