Блог · Security · Code Review

Разбор типичных находок ручного код-ревью: SQL-инъекции, XSS, broken auth, IDOR, race conditions, утечки секретов — с примерами кода и рекомендациями.

2026-08-28 · 2 мин чтения

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 дней. По итогам вы получаете:

  • Отчет с описанием каждой уязвимости: место в коде, сценарий эксплуатации, приоритет, рекомендация по исправлению.
  • Анализ работы с секретами, сессиями, авторизацией и бизнес-логикой.
  • Разбор архитектурных рисков: разделение доступов, изоляция компонентов, обработка ошибок.
  • Возможность обсудить находки голосом и помощь с исправлениями, потому что я разработчик, а не только ревьюер.

Цены и объем — на главной странице. Если хотите, чтобы ваш код посмотрели глазами того, кто его пишет, напишите через форму на главной странице.