Разбор типичных находок ручного код-ревью: SQL-инъекции, XSS, broken auth, IDOR, race conditions, утечки секретов — с примерами кода и рекомендациями.
Security code review backend-приложений: какие уязвимости находятся чаще всего
Автоматические сканеры стали стандартом: SAST в пайплайне, SCA для зависимостей, DAST по расписанию. Это правильная база, но у сканеров есть принципиальное ограничение: они ищут известные паттерны. Логическая ошибка в бизнес-логике, race condition в платежном сценарии, неочевидная цепочка от параметра в HTTP-заголовке до SQL-запроса — это то, что сканер пропускает, а человек находит.
Я провожу ручной security code review backend-приложений. Ниже — разбор классов уязвимостей, которые находятся в коде чаще всего, с примерами того, как это выглядит в коде и как исправлять.
SQL-инъекции: живы и опасны
Классика не потому, что разработчики не знают про prepared statements, а потому, что параметризация легко теряется в частных местах.
Было:
$sort = $_GET['sort'] ?? 'name';
$sql = "SELECT * FROM products ORDER BY $sort";
Параметризовать ORDER BY нельзя, и в этом месте разработчик часто вставляет значение напрямую. Атакующий передает sort=(SELECT CASE WHEN(условие) THEN col1 ELSE col2 END), и через ошибки или тайминги выдергивает данные из любой таблицы.
Стало:
$allowed = ['name' => 'name', 'price' => 'price'];
$sort = $allowed[$_GET['sort'] ?? 'name'] ?? 'name';
$sql = "SELECT * FROM products ORDER BY $sort";
Белый список решает проблему: в запрос попадает только значение из фиксированного набора.
Где чаще всего встречается:
- Динамические ORDER BY, имена таблиц и колонок, LIMIT.
- Поиск: подстрока вставляется в LIKE через конкатенацию.
- Легаси-код, где ORM соседствует с голым SQL.
- Экспорт в CSV или отчеты, где фильтры собираются строкой.
XSS: экранирование теряется в шаблонах
Backend-разработчики привыкли, что шаблонизатор экранирует сам. Но XSS живет в местах, где экранирование не срабатывает.
Типичные находки:
- Значение вставляется в атрибут
hrefилиsrc: экранирование HTML не спасает отjavascript:alert(1). Нужна проверка схемы. - JSON вставляется в тег script для передачи данных в клиент:
</script>внутри JSON закрывает тег и открывает путь к внедрению. Нужна сериализация с экранированием</или отдельная передача через data-атрибут. - Значение попадает в
onclick="handler('...')": экранирование для HTML-контекста не работает внутри JavaScript-строки.
Было:
<a href="<?= $user['website'] ?>">Сайт</a>
Стало:
<?php $url = filter_var($user['website'], FILTER_VALIDATE_URL) ? $user['website'] : '#'; ?>
<a href="<?= htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ?>">Сайт</a>
Аутентификация: не только про пароли
Broken authentication в ревью редко выглядит как отсутствие авторизации. Чаще это тонкие места.
Что находю:
- Токен сброса пароля генерируется без криптографической случайности:
substr(md5(uniqid()), 0, 8)подбирается за минуты. Нуженrandom_bytes(32). - Токен не имеет срока действия или не инвалидируется после использования.
- Сессия не инвалидируется при смене пароля: украденная сессия переживает смену пароля.
- Лимит попыток входа считается по имени пользователя, а не по паре пользователь+IP, и не сбрасывается при успехе, что позволяет блокировать чужие аккаунты.
IDOR: забытая проверка владельца
IDOR и BOLA в API — самая частая находка ручного ревью. Механика всегда одна: эндпоинт проверяет, что пользователь авторизован, но не проверяет, что объект принадлежит именно ему.
Было:
$order = $db->query("SELECT * FROM orders WHERE id = ?", [$id])->fetch();
return $order;
Стало:
$order = $db->query(
"SELECT * FROM orders WHERE id = ? AND user_id = ?",
[$id, $currentUser->id]
)->fetch();
if (!$order) {
http_response_code(404);
exit;
}
Где чаще всего встречается: выгрузка документов и счетов по ID, API для мобильных приложений, эндпоинты админки, где проверка роли есть на уровне раздела, но не на уровне объекта, импорт и экспорт файлов.
Race conditions: логика без транзакций
Race conditions находят только ручным ревью, потому что для их обнаружения нужно понять бизнес-логику, а не сопоставить паттерн.
Показательный пример из практики: интернет-магазин с бонусным балансом. Проверка баланса и списание были разнесены по коду, без транзакции и блокировки. Два одновременных запроса на покупку проходили проверку баланса оба, и списание уходило в минус. Классический TOCTOU, автосканер такое не находит никогда.
Было:
$balance = $db->query("SELECT balance FROM accounts WHERE id = ?", [$userId])->fetchColumn();
if ($balance < $price) {
throw new Exception('Недостаточно средств');
}
$db->query("UPDATE accounts SET balance = balance - ? WHERE id = ?", [$price, $userId]);
Стало:
$db->beginTransaction();
try {
$row = $db->query(
"SELECT balance FROM accounts WHERE id = ? FOR UPDATE",
[$userId]
)->fetch();
if ($row['balance'] < $price) {
throw new Exception('Недостаточно средств');
}
$db->query("UPDATE accounts SET balance = balance - ? WHERE id = ?", [$price, $userId]);
$db->commit();
} catch (Exception $e) {
$db->rollBack();
throw $e;
}
Ключевое: проверка и изменение выполняются атомарно, строка заблокирована FOR UPDATE до конца транзакции. Та же механика нужна в купонах, лимитах, квотах, остатках на складе.
Секреты и сессии
Что находю в ревью:
- Секреты в репозитории: ключи API, пароли БД в конфиге рядом с кодом. Даже если репозиторий приватный, доступ к нему есть у всех разработчиков, а история хранит секреты навсегда.
- Секреты в логах: запрос с токеном или паролем попадает в access log или в трейс исключения.
- Сессия не перегенерируется после входа: фиксация сессии. Атакующий подкладывает жертве известный ему идентификатор сессии, жертва входит, атакующий пользуется той же сессией.
- Cookie сессии без флагов Secure и HttpOnly.
Было:
setcookie('session', $token);
Стало:
setcookie('session', $token, [
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
'path' => '/',
]);
session_regenerate_id(true);
Что входит в платный Security Code Review
Ручной аудит исходного кода backend-приложения за 5–7 дней. По итогам вы получаете:
- Отчет с описанием каждой уязвимости: место в коде, сценарий эксплуатации, приоритет, рекомендация по исправлению.
- Анализ работы с секретами, сессиями, авторизацией и бизнес-логикой.
- Разбор архитектурных рисков: разделение доступов, изоляция компонентов, обработка ошибок.
- Возможность обсудить находки голосом и помощь с исправлениями, потому что я разработчик, а не только ревьюер.
Цены и объем — на главной странице. Если хотите, чтобы ваш код посмотрели глазами того, кто его пишет, напишите через форму на главной странице.