Владелец проверил страницу оплаты и не заметил ничего необычного. А покупатель увидел прямо в магазине поля для ввода номера карты и CVC, хотя обычно сайт переводил его к платёжному провайдеру. Расследование началось с этого различия и привело к чужому JavaScript, сохранённому в базе WordPress.
Покупатель увидел форму, которой в магазине не должно быть
Сначала владелец получил сообщение от покупателя. При выборе оплаты картой на странице заказа появлялись поля для номера карты, срока действия и CVC. Это выглядело странно: магазин обычно перенаправлял клиента на страницу платёжного провайдера, а банковские реквизиты на своём сайте не запрашивал.
Владелец открыл корзину под своей учётной записью. Товары добавлялись, сумма пересчитывалась, оформление заказа шло как обычно, а переход к провайдеру работал. Никаких лишних полей. Под подозрение сначала попал платёжный модуль, но оставался вопрос: почему покупатель видит то, чего не видит администратор?
Что было известно о магазине
[woocommerce_checkout].На время расследования отключили затронутый способ оплаты
Неожиданные поля карты были достаточным основанием, чтобы приостановить затронутый способ оплаты. Это согласовали с владельцем и ответственными за платежи. Отключение было временной мерой: источник подмены ещё предстояло найти и удалить.
Перед очисткой сохранили копии файлов и базы, настройки платёжного модуля, время жалобы и доступные журналы сервера и административных действий. Отдельно зафиксировали вид страницы и сведения о подозрительном скрипте. Материалы хранили вне публичной части сайта.
Одна страница — два разных результата
Сравнили поведение страницы в административном и чистом гостевом сеансах. Товар, доставка и способ оплаты были одинаковыми. У администратора лишних полей не было, у гостя они появлялись после выбора оплаты. Эти наблюдения стали отправной точкой для дальнейшего разбора на изолированной копии магазина.

| Условие | Что увидели | Что это подсказало |
|---|---|---|
| Администратор | Оплата выглядит нормально, посторонних полей нет. | Проверять только под учётной записью владельца нельзя. |
| Гость в новом браузере | После выбора оплаты появляются дополнительные поля карты. | Проблема воспроизводилась вне браузера покупателя; версия о его расширении стала менее вероятной. |
| Второй гостевой сеанс | Та же форма появляется ещё раз. | Случайный сбой браузера маловероятен. |
| Настройки платёжного модуля | Магазин должен перенаправлять к провайдеру, а не запрашивать CVC. | Наблюдаемая форма не соответствовала настроенному сценарию оплаты. |
Проверка настроек платёжного модуля и кеша не объяснила различие. Поэтому следующим шагом стало сравнение кода и сетевых запросов страницы: нужно было понять, что получает браузер гостя.
След привёл к постороннему JavaScript
В инструментах разработчика браузера (DevTools) сопоставили исходный HTML с разметкой после загрузки страницы и выбора оплаты. В исходном ответе полей карты не было, а в разметке, которую видел гость, они появлялись. Это указывало на добавление формы клиентским кодом; далее искали конкретный скрипт.
В списке запросов на вкладке Network обнаружили загрузку JavaScript с постороннего домена. Среди согласованных сервисов оплаты и аналитики этого адреса не было. В сведениях об инициаторе запроса — Initiator — прослеживалась короткая вставка JavaScript на странице заказа. По ней и продолжили поиск.

Как удалось связать скрипт с формой
Сам по себе незнакомый домен не доказывает кражу данных. Здесь подозрение подтвердилось содержимым скрипта: он создавал поля карты, читал введённые значения и готовил их отправку на адрес, не связанный с платёжным провайдером. Такая логика объясняла назначение подмены, но ещё не показывала, сколько покупателей успели с ней столкнуться и была ли передача успешной.
Загрузчик нашли в записи плагина, сохранённой в базе
В активной теме проверили functions.php и шаблоны оформления заказа. Подключения к найденному домену там не обнаружили. Это сузило поиск, но само по себе ещё не исключало другие файлы или компоненты WordPress.
Искомая вставка нашлась среди сохранённых фрагментов плагина пользовательского кода. Такой плагин позволяет добавлять CSS и JavaScript без редактирования темы. В данном случае одна его запись в базе подключала чужой JavaScript на странице оплаты. Найденный фрагмент совпадал со вставкой, которую до этого увидели в браузере.

| Объект | Наблюдение | Вывод |
|---|---|---|
| Файлы темы | В шаблонах темы подключение постороннего скрипта не нашли. | В проверенных шаблонах источник не нашли; поиск продолжили в других компонентах. |
| Плагин пользовательского кода | В проверенных файлах плагина изменений не обнаружили; подключение JS задавала сохранённая запись. | Плагин выводил код из базы. Способ изменения записи требовал отдельной проверки. |
| База WordPress | В записи нашли тот же загрузчик и условие, ограничивавшее его запуск гостевыми сеансами. | Это объяснило, почему администратор не видел форму. |
| Кеш/CDN | Проверяли, не сохранилась ли вставка в кешированных ответах и ресурсах. | Кеш учитывали при очистке и повторной проверке; источником внедрения оставалась запись в базе. |
Проверка файлов не охватывала содержимое этой записи. Поэтому переустановка расширения без проверки сохранённых данных не давала оснований считать подмену устранённой.
Отключение записи убрало загрузчик со страницы
Условие запуска в сохранённом фрагменте совпало с наблюдаемым поведением: в административном сеансе загрузчик не срабатывал, а гостю подключал внешний скрипт. Анализ этого скрипта, в свою очередь, связал его с появлением полей карты.
На изолированной копии отключили только найденную запись. Тему и платёжный модуль оставили прежними. После этого вставка исчезла из HTML, а попытки загрузить связанный с ней скрипт прекратились. Ограничения на внешние соединения в ходе сравнения не меняли. Так проверили источник подключения; отсутствие поддельной формы после очистки и работу штатной оплаты проверили отдельно.
Какие факты подтвердили источник
| Признак | Что он подтверждает |
|---|---|
| Форма появляется только у гостей | Задаёт условия воспроизведения. Сам по себе этот признак не раскрывает способ взлома. |
| Initiator указывает на вставку | Связывает сетевой запрос с кодом на странице. |
| В базе нашлась та же вставка | Связывает код в ответе магазина с сохранённой записью плагина. |
| После отключения исчезает загрузчик | При неизменных остальных условиях подтверждает, какая запись добавляла вставку на страницу. |
| Оплата работает без замены шлюза | Для устранения найденной подмены замена платёжного модуля не потребовалась. |
Совпадение кода в базе и на странице, разбор внешнего скрипта и отключение конкретной записи связали источник с подменой. Теперь нужно было проверить административный доступ: удалённый фрагмент мог появиться снова, если возможность его добавить оставалась у постороннего.
Что показал журнал административных действий
Историю изменения фрагмента сопоставили с доступным журналом действий в WordPress. Изменение было записано от имени администратора, при этом параметры сеанса отличались от обычных входов владельца. Это дало основание проверить использование привилегированной учётной записи, но не позволяло установить человека за ней или способ получения доступа.
Проверили пользователей WordPress и их права, доступы к хостингу и SFTP/SSH, изменения расширений, подключённые менеджеры тегов и настройки CDN. Отдельно пересмотрели, кто может добавлять и менять пользовательский код.
Для этой учётной записи одной сменой пароля не ограничились. Завершили активные административные сеансы, пересмотрели права, включили многофакторную аутентификацию и обновили связанные секреты и доступы в пределах выявленного риска.

Удалили запись и проверили, нет ли других мест внедрения
На защищённой копии проверили другие места, где мог сохраниться посторонний код. Затем удалили вредоносную запись на рабочем сайте и внесли согласованные изменения. Текущую базу старой резервной копией не заменяли. Затронутую оплату возвращали к работе после проверки оформления заказа.
Зачем понадобилась проверка файлов
Контрольные суммы помогли сопоставить файлы ядра и плагинов, для которых были доступны эталоны WordPress.org. Результат этой проверки не распространялся на записи базы и не заменял проверку остальных компонентов.
wp core verify-checksums --include-root wp plugin verify-checksums --all
Этими командами проверяли совпадение файлов с доступными эталонами. Они не удаляют вредоносный код и не подтверждают чистоту всего сайта. Отсутствие контрольных сумм у платного или собственного компонента также не означает заражение: для него нужен другой эталон — например, чистый пакет поставщика. Проверку проводили на защищённой копии из доверенного окружения.
Отдельно проверили PHP-файлы в каталоге загрузок, mu-plugins и задания по расписанию. Других активных компонентов, связанных с обнаруженной подменой, в проверенных местах не нашли.
Сохранили заказы и проверили платёжную цепочку
Полный откат базы мог затронуть заказы, оплаты, возвраты и остатки, изменившиеся после создания резервной копии. Поэтому старую копию использовали для анализа, а вредоносную запись удаляли из действующей базы отдельно.
Перед сверкой определили действующее хранилище заказов WooCommerce и настройки синхронизации. Это существенно при HPOS: рабочие данные могут находиться в специальных таблицах, а записи в wp_posts — не быть основным источником. Сверку проводили по активному хранилищу.
| Область | Результат проверки |
|---|---|
| Заказы и статусы | Сверили заказы, платежи, возвраты и незавершённые операции. |
| Хранилище заказов | Определили активный режим хранения и проверили связанные настройки синхронизации. |
| Фоновые операции | Проверили задания Action Scheduler, письма и обмен данными с другими сервисами. |
| Платёжные события | Проверили адреса вебхуков, секреты и получение уведомлений о платеже. |
| Каталог | Сверили остатки, резервирование товаров и скидки. |
После очистки провели оплату в разрешённом тестовом режиме провайдера. Проверили создание заказа, смену статуса, уведомления, отказ в оплате, обработку возврата и связанные фоновые действия. Настоящие банковские карты для испытаний не использовали.
Могли ли злоумышленники получить данные карт?
Код позволял читать значения поддельных полей и готовил отправку на посторонний адрес. Поэтому наряду с восстановлением магазина отдельно оценивали возможное воздействие на покупателей до обнаружения подмены.
Дату изменения записи сопоставили с журналами и жалобами покупателей. Эти сведения давали ориентиры для оценки периода, но не гарантировали точную дату начала заражения. Кроме того, запросы из браузера на чужой сервер могли вообще не попадать в логи магазина. Отсутствие таких записей не позволяло исключить передачу данных.
Подмена исчезла, штатное оформление заказа заработало
После удаления записи, очистки кеша и обновления доступов вернулись к исходному сценарию покупателя. В проверенных гостевых сеансах лишних полей больше не было, переход к платёжному провайдеру работал, а обнаруженный ранее скрипт не загружался.

| Проверка | Что получилось | Чем проверили |
|---|---|---|
| Гостевое оформление заказа | В проверенных сеансах лишних полей карты не было. | Повторили сценарий, в котором покупатель видел подмену. |
| Подключённые скрипты | Найденная вставка и подключение чужого скрипта отсутствовали. | Сверили HTML и сетевые запросы страницы после очистки. |
| Оплата и заказы | Заказ создавался, статус оплаты обновлялся, уведомление приходило. | Провели проверку в тестовом режиме платёжного провайдера. |
| База и файлы | Вредоносная запись удалена; других связанных компонентов в проверенных местах не нашли. | Проверили сохранённые фрагменты, файлы и задания по расписанию. |
| Привилегии | Административные сеансы завершены, доступы и права пересмотрены. | Проверили учётные записи и разрешения на изменение пользовательского кода. |
Для страницы оформления заказа настроили дальнейший контроль изменений и сетевых подключений. Результат расследования сформулировали по проверенным фактам: источник найден и удалён, штатная оплата работает. Способ получения административного доступа и возможную передачу данных до очистки не объявляли установленными.
Почему было важно найти источник, а не просто убрать форму
В этом расследовании решающим оказалось различие между гостевым и административным сеансами. Проверка только под владельцем скрывала симптом, а проверка только файлов пропускала загрузчик в базе.
К источнику привели наблюдение в браузере, инициатор сетевого запроса и совпадение вставки с записью плагина. После её удаления оставалось проверить доступы и нормальную обработку заказов. Именно это отделяло устранение видимого симптома от восстановления магазина.
Что ещё спросит владелец магазина
Почему владелец не заметил заражение сразу?
Скрипт проверял, авторизован ли посетитель. Под администратором подмена не включалась. Поэтому владелец видел обычную оплату, а покупатели — чужую форму.
Почему проверка файлов WordPress не выявила причину?
В проверенных файлах плагина изменений не обнаружили. Загрузчик хранился в записи базы, которую плагин выводил как пользовательский JavaScript. Сверка файлов не проверяет содержимое таких записей.
Значит ли это, что плагин пользовательского кода был уязвим?
Нет. Сам факт появления записи не доказывает уязвимость расширения. В доступном журнале изменение было связано с административной учётной записью, но способ получения доступа остался неустановленным.
Достаточно ли очистить базу и кеш?
Недостаточно удалить одну запись и очистить кеш. Нужно проверить другие места внедрения и привилегированные доступы, а затем убедиться, что оформление заказа, оплата и фоновые операции работают корректно.
Можно ли обещать, что данные карт не были похищены?
Нет. Исчезновение формы говорит о текущем состоянии сайта. Были ли раньше отправлены данные карты, приходится выяснять отдельно — по доступным следам и вместе с платёжным провайдером.
Другой вариант скрытого заражения — SEO-спам в поисковой выдаче. Он может оставаться незаметным при обычном просмотре сайта, но цель и способ проверки отличаются: в этом случае мы расследовали вмешательство в оплату покупателя.