
Технический бюллетень и proof of concept для CVE-2026-97286 — аутентифицированный хранимый XSS в плагине WordPress Strong Testimonials, с цепочкой атаки, PoC и анализом патча.
Аутентифицированный пользователь с ролью Contributor может сохранить контролируемые злоумышленником метаданные фото платформы в плагине WordPress Strong Testimonials. До версии 3.3.12 плагин выводил сохранённое значение внутри атрибута src изображения без контекстного экранирования. После того как администратор публикует созданный отзыв и запись, отображающую его, полезная нагрузка выполняется у посетителей, открывших эту запись, включая неаутентифицированных посетителей.
Это исследование проводилось в авторизованной локальной лаборатории и было раскрыто через Patchstack до публичного релиза.
| Поле | Значение |
|---|---|
| CVE | CVE-2026-97286 |
| Продукт | Strong Testimonials |
| Вендор | WP Chill |
| Слаг плагина | strong-testimonials |
| Уязвимость | Аутентифицированный хранимый межсайтовый скриптинг |
| Затронутые версии | <= 3.3.11 |
| Исправленная версия | 3.3.12 |
| Требуемая привилегия | Contributor или выше |
| CWE | CWE-79: Ненадлежащая нейтрализация входных данных при генерации веб-страницы |
| CVSS v3.1 | 6.5 Medium — CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:L |
| Исследователь | Rully Miftahur Rozaq |
| Дата отчёта | 16 сентября 2026 г. |
| Публичное раскрытие | 30 сентября 2026 г. |
Patchstack классифицирует рекомендацию как низкоприоритетную, несмотря на то что присвоенная базовая оценка серьёзности CVSS v3.1 — Medium.
Редактор отзывов принимает массив запроса custom и сохраняет каждый переданный ключ как метаданные записи. Ключи, отсутствующие в настроенном определении пользовательского поля, проходят через sanitize_text_field(). Это позволяет пользователю с ролью Contributor передавать зарезервированные плагином метаданные, такие как platform и platform_user_photo, при редактировании собственного отзыва.
Для поддерживаемых социальных платформ плагин извлекает platform_user_photo, проверяет, что URL возвращает HTTP-ответ 200, а затем подставляет исходную сохранённую строку в атрибут <img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/...">. Проверка HTTP-статуса не делает строку безопасной для HTML-атрибута. Кавычка во фрагменте URL может завершить src и внедрить обработчик события.
Подтверждённая цепочка атаки:
Внедрение является аутентифицированным. Посетителю, который запускает уже опубликованную полезную нагрузку, не нужно проходить аутентификацию.
Успешная эксплуатация обеспечивает постоянное выполнение JavaScript в источнике затронутого WordPress на страницах, которые отображают созданный отзыв через уязвимый путь миниатюры. Код выполняется с привилегиями браузера и сессии WordPress посетителя, если таковая имеется.
Воспроизведённый путь требует публикации администратором, что объясняет метрику CVSS UI:R. В этом исследовании не были продемонстрированы неаутентифицированное внедрение, автоматическое выполнение на каждой странице сайта, повышение привилегий, кража cookie или полная компрометация сайта.
Proof of concept был воспроизведён со следующей конфигурацией:
| Компонент | Конфигурация |
|---|---|
| WordPress | 7.1 |
| Strong Testimonials | 3.3.7, бесплатная редакция |
| Тема | Twenty Twenty-Five |
| Шаблон отображения | default-theme |
| Миниатюра | Включена |
| PHP / Apache | PHP 8.3.33 / Apache 2.4.68 |
| База данных | MariaDB 11.4 |
| Роль атакующего | Contributor |
| Возможности атакующего | edit_posts=true, publish_posts=false, unfiltered_html=false |
Версия 3.3.7 была точной версией, использованной в лаборатории. Публичная запись CVE определяет все версии до 3.3.11 включительно как затронутые, а версию 3.3.12 — как исправленную.
В admin/class-strong-testimonials-post-editor.php метод save_details() перебирает каждую запись в отправленном массиве custom. Нераспознанный ключ попадает в общую ветку санитизации и затем сохраняется с помощью update_post_meta():
$custom = $_POST['custom'];
foreach ( $custom as $key => $value ) {
// ...
$sanitized_value = sanitize_text_field( $value );
update_post_meta( $post_id, $key, $sanitized_value );
}
Настоящий обработчик выполняет проверки nonce и санитизацию для конкретных полей. Находка не полагается на обход этих проверок или редактирование чужого отзыва. Участник (Contributor) правомерно редактирует свой собственный отзыв, но передаёт зарезервированные ключи platform и platform_user_photo.
В уязвимых версиях includes/functions-image.php читает platform_user_photo, выполняет проверку HTTP-статуса через wpmtst_is_valid_image_url() и вставляет исходную строку в HTML-атрибут:
$platform_user_photo = get_post_meta( $post_id, 'platform_user_photo', true );
if ( ! $platform_user_photo || ! wpmtst_is_valid_image_url( $platform_user_photo ) ) {
return $img;
}
return sprintf(
'<img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/%25s" %s %s/>',
$platform_user_photo,
$width ? "width='{$width}'" : '',
$height ? "height='{$height}'" : ''
);
Фрагментная часть URL не отправляется на HTTP-сервер, но остаётся в строке, которая позже обрабатывается PHP. Таким образом, сервер может видеть безобидный URL, возвращающий HTTP 200, в то время как браузер получает значение, содержащее последовательность для выхода из атрибута.
Шаблон default-theme вызывает wpmtst_get_thumbnail() и напрямую выводит возвращённую разметку изображения:
<figure class="wp-block-image wpmtst-testimonial-avatar">
<?php echo $wpmtst_avatar_html; ?>
</figure>
Шаблон предполагает, что помощник уже сгенерировал безопасный HTML. В затронутых версиях помощник фотографий платформы не экранировал URL для своего контекста src.
Воспроизводите эту проблему только в системе, которой вы владеете или которую вам явно разрешено тестировать.
<= 3.3.11 на авторизованном тестовом сайте WordPress.default-theme с включёнными миниатюрами.Войдите как Contributor и создайте новый черновик отзыва.
Перехватите легитимный запрос сохранения POST /wp-admin/post.php с помощью перехватывающего прокси. Сохраните его полное тело формы, cookie сессии, свежий nonce WordPress, nonce плагина и ID отзыва, принадлежащего Contributor.
Добавьте эти параметры в тело формы:
custom[platform]=facebook
custom[platform_user_photo]=http://wordpress/index.php?rest_route=/#" onerror="alert(1)" x="
URL-кодированный фрагмент формы:
&custom%5Bplatform%5D=facebook&custom%5Bplatform_user_photo%5D=http%3A%2F%2Fwordpress%2Findex.php%3Frest_route%3D%2F%23%22%20onerror%3D%22alert%281%29%22%20x%3D%22
Имя хоста wordpress — это имя Docker-сервиса, используемое в локальной лаборатории. В другой авторизованной среде замените его на тестовую конечную точку, контролируемую исследователем, которая доступна из WordPress и возвращает HTTP 200. Не направляйте запросы на сторонние или чувствительные внутренние сервисы.