روز ۲۱ اکتبر ۲۰۲۶ (۲۹ مهر ۱۴۰۵) تیم امنیت PHP نسخه‌های 8.5.12، 8.4.27، 8.3.36 و 8.2.35 را منتشر کرد. این نسخه‌ها دو آسیب‌پذیری بحرانی را می‌بندند که به مهاجم اجازه می‌دهند بدون login و از راه دور روی سرور کد اجرا کند. شماره‌های آن‌ها CVE-2026-109618 و CVE-2026-109619 است و در جامعه امنیتی با نام php2shell شناخته می‌شوند. این مقاله راهنمای عملی به‌روزرسانی امنیتی PHP برای همین دو آسیب‌پذیری است: چه کسی در خطر است، چطور مطمئن شوید سرورتان واقعاً پچ شده، و اگر نمی‌توانید همین امروز به‌روزرسانی کنید چه کار کنید.

من این آسیب‌پذیری را خودم آزمایش نکرده‌ام و قصد چنین کاری هم ندارم. اطلاعات فنی از اطلاعیه رسمی php.net، advisoryهای مخزن php-src در GitHub، وبلاگ PHP Foundation، هشدار CISA، ردیاب CVE اوبونتو و راهنمای Laravel Forge آمده است. فهرست منابع در انتهای مقاله است. بخش‌هایی که خودم اجرا کرده‌ام، یعنی اسکریپت بررسی نسخه و تفاوت imageهای Docker، صریحاً مشخص شده‌اند.

خلاصه در یک نگاه

شناسهمشکلشدتنسخه امن
CVE-2026-109618اجرای کد از راه دور با خرابی حافظه هنگام out-of-memoryCVSS 4.0: ۹٫۲8.5.12 / 8.4.27 / 8.3.36 / 8.2.35
CVE-2026-109619اجرای کد از راه دور با وضعیت global کهنه بین درخواست‌هاCVSS 4.0: ۹٫۲

طبق اطلاعیه رسمی، PHP 8.1 و نسخه‌های قدیمی‌تر از پروژه PHP پچی دریافت نمی‌کنند. اگر روی این نسخه‌ها هستید، فقط توزیع لینوکس یا فروشنده پشتیبانی بلندمدت شما می‌تواند پچ بدهد. PHP 8.2 هم ۳۱ دسامبر ۲۰۲۶ به پایان پشتیبانی می‌رسد.

چه کسانی در خطرند؟

اطلاعیه php.net می‌گوید همه نصب‌های PHP که درخواست وب پاسخ می‌دهند در معرض خطرند، از جمله PHP-FPM، Apache با mod_php، php-cgi، FrankenPHP و LiteSpeed. سطح ریسک یکسان نیست:

  • Apache با mod_php: بیشترین ریسک. طبق advisory، روی این حالت exploit نمایش داده شده که یک درخواست بدون احراز هویت می‌تواند قبل از اجرای هر کدی از برنامه شما حمله را انجام دهد. یعنی Laravel، WordPress یا هر framework دیگری نمی‌تواند جلوی آن را بگیرد.
  • PHP-FPM و FrankenPHP: advisory آن‌ها را «احتمالاً آسیب‌پذیر» می‌داند. بیشتر سرورهای Laravel با nginx و PHP-FPM در همین دسته‌اند. «احتمالاً» دلیل تأخیر نیست.
  • PHP فقط در خط فرمان (اسکریپت‌های cron، queue worker) مستقیماً درخواست وب از اینترنت نمی‌گیرد. اما معمولاً همان پکیج و همان باینری روی همان سرور است، پس با بقیه به‌روزرسانی می‌شود.

CISA تا زمان انتشار هشدار خود، بهره‌برداری فعال در اینترنت مشاهده نکرده بود. با این حال شناسه CVEها دو روز زودتر از موعد نشت کرد و جزئیات فنی اکنون عمومی است. نباید منتظر گزارش اولین حمله ماند.

دو باگ به زبان ساده

CVE-2026-109618: خرابی حافظه هنگام کمبود حافظه

PHP وقتی به memory_limit می‌رسد، با پرش (longjmp) از وسط کار خارج می‌شود. طبق advisory، اگر این اتفاق دقیقاً وقتی بیفتد که داده‌های داخلی درخواست هنوز نیمه‌کاره ساخته شده‌اند، مرحله پاک‌سازی انتهای درخواست آن داده نیمه‌کاره را معتبر فرض می‌کند و حافظه خراب می‌شود. مهاجم می‌تواند با درخواست‌های multipart دست‌کاری‌شده، مثلاً تعداد زیاد header در هر بخش یا فیلدهای فایل خالی، مصرف حافظه را در مرحله parse بدنه درخواست و قبل از رسیدن به کد شما کنترل کند. پچ رسمی رفتار OOM را عوض کرده است: به‌جای پرش، خطای حافظه با تأخیر و در حالت شدید با خروج کامل پردازه مدیریت می‌شود.

CVE-2026-109619: وضعیت کهنه بین دو درخواست

worker در PHP-FPM یا mod_php بعد از هر درخواست از بین نمی‌رود و درخواست بعدی را جواب می‌دهد. advisory توضیح می‌دهد بعضی وضعیت‌های global، مثل وضعیت خطا، session و افزونه‌ها، ممکن است بعد از خطای fatal یا OOM هنوز به حافظه درخواست قبلی اشاره کنند. اگر درخواست بعدی پیش از مقداردهی دوباره به همان وضعیت دسترسی پیدا کند، از حافظه آزادشده استفاده می‌شود (use-after-free). دو «تولیدکننده» تأییدشده عبارت‌اند از backtrace خطای fatal وقتی fatal_error_backtraces روشن است (در PHP 8.5)، و چرخه session.upload_progress وقتی با OOM یا خطای fatal قطع می‌شود.

گام ۱: همه جاهایی که PHP اجرا می‌شود را پیدا کنید

بیشتر تیم‌ها سرور اصلی را به یاد دارند و بقیه را فراموش می‌کنند. فهرست را کامل کنید:

  • سرورهای وب production و staging، و همه نسخه‌های PHP نصب‌شده روی هر سرور (ls /etc/php)، حتی نسخه‌هایی که هیچ سایتی از آن‌ها استفاده نمی‌کند.
  • imageهای Docker، چه در registry خودتان و چه imageهایی که CI از روی آن‌ها build می‌کند.
  • هاست اشتراکی و پنل‌هایی مثل cPanel یا DirectAdmin که نسخه PHP را خود میزبان مدیریت می‌کند.
  • ابزارهای داخلی کمتر دیده‌شده: phpMyAdmin، پنل مانیتورینگ، WordPress وبلاگ شرکت.

گام ۲: نسخه را درست بخوانید (دامی که اکثراً در آن می‌افتند)

دستور php -v برای PHP کامپایل‌شده از سورس، imageهای رسمی Docker و پکیج‌های ondrej/sury کافی است: اگر عدد کمتر از نسخه امن باشد، آسیب‌پذیرید. اما پکیج‌های رسمی خود Ubuntu و Debian پچ را روی همان نسخه قدیمی backport می‌کنند و عدد PHP عوض نمی‌شود. روی یک سرور Ubuntu 24.04 که بررسی کردم، php -v عدد 8.3.6 را نشان می‌داد. این عدد هم قبل و هم بعد از پچ همین است. چیزی که تفاوت را نشان می‌دهد نسخه پکیج است:

dpkg-query -W -f='${Package} ${Version}\n' php8.3-fpm php8.3-cli
# php8.3-fpm 8.3.6-0ubuntu0.24.04.12   ← آسیب‌پذیر
# نسخه امن Ubuntu 24.04 طبق USN-8948:  8.3.6-0ubuntu0.24.04.13

طبق ردیاب CVE اوبونتو، پچ برای php8.3 روی Ubuntu 24.04 در نسخه پکیج 8.3.6-0ubuntu0.24.04.13 و برای php8.1 روی Ubuntu 22.04 در 8.1.2-1ubuntu2.28 آمده است (اطلاعیه‌های USN-8948-1 و USN-8948-2، ۲۱ اکتبر). روی همان سرور unattended-upgrades فعال بود و صبح ۲۱ اکتبر PHP را به‌روز کرده بود، اما آن اجرا پیش از انتشار پچ بود و اجرای صبح روز بعد هم هنوز نسخه جدید را نگرفته بود. پس «به‌روزرسانی خودکار فعال است» به معنای «پچ نصب شده» نیست. راهنمای Laravel Forge هم صریحاً می‌گوید به‌روزرسانی امنیتی خودکار Forge، PHP را پچ نمی‌کند، چون PHP از مخزن جداگانه نصب شده است.

دام دوم: image قدیمی Docker در cache

روی سیستم خودم دو image رسمی از همان شاخه داشتم. php:8.3-cli-alpine که امروز تازه pull شد نسخه 8.3.36 بود، اما php:8.3-fpm-alpine که چند روز قبل pull شده بود هنوز 8.3.33 بود. tag شناور 8.3 فقط وقتی به نسخه جدید اشاره می‌کند که دوباره pull کنید. docker build معمولی از image موجود در cache استفاده می‌کند. برای ساخت دوباره از --pull استفاده کنید و نسخه را در خود container چک کنید.

اسکریپت بررسی سریع

این اسکریپت را نوشتم و روی PHP 8.2.28 محلی، دو image Docker و یک سرور Ubuntu اجرا کردم. نسخه را با جدول نسخه‌های امن مقایسه می‌کند، build توزیع را تشخیص می‌دهد و مقدار تنظیمات مرتبط با راهکارهای موقت را نشان می‌دهد:

<?php
$fixed = ['8.2' => '8.2.35', '8.3' => '8.3.36', '8.4' => '8.4.27', '8.5' => '8.5.12'];
$branch = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
$distroBuild = (bool) preg_match('/ubuntu|deb/i',
    PHP_EXTRA_VERSION . php_uname('v') . (string) @file_get_contents('/etc/os-release'));

printf("PHP %s (%s)\n", PHP_VERSION, PHP_SAPI);
if (!isset($fixed[$branch])) {
    echo "UNSUPPORTED: no upstream fix for {$branch}\n";
} elseif (version_compare(PHP_VERSION, $fixed[$branch], '>=')) {
    echo "PATCHED: {$branch} >= {$fixed[$branch]}\n";
} elseif ($distroBuild) {
    echo "CHECK PACKAGE: distro build, compare with your distro's CVE tracker\n";
} else {
    echo "VULNERABLE: upgrade to {$fixed[$branch]}\n";
}
foreach (['memory_limit', 'post_max_size', 'session.upload_progress.enabled'] as $k) {
    printf("  %-32s %s\n", $k, var_export(ini_get($k), true));
}
محیطخروجی
PHP 8.2.28 نصب‌شده با HomebrewVULNERABLE: upgrade to 8.2.35
php:8.3-cli-alpine تازه pull شدهPATCHED: 8.3 >= 8.3.36
php:8.3-fpm-alpine قدیمی در cacheVULNERABLE: upgrade to 8.3.36
Ubuntu 24.04 با پکیج رسمیCHECK PACKAGE (باید نسخه پکیج را چک کرد)

نکته: CLI و FPM ممکن است فایل php.ini جدا داشته باشند. اسکریپت را با CLI اجرا کنید تا نسخه باینری را ببینید. برای تنظیمات FPM از php-fpm8.3 -i استفاده کنید یا اسکریپت را موقتاً از مسیر وب و پشت احراز هویت اجرا کنید و بلافاصله حذف کنید.

گام ۳: به‌روزرسانی

Ubuntu با پکیج رسمی توزیع:

sudo apt-get update
apt-cache policy php8.3-fpm        # Candidate باید 8.3.6-0ubuntu0.24.04.13 یا بالاتر باشد
sudo apt-get install --only-upgrade -y $(dpkg -l 'php8.3*' | awk '/^ii/ {print $2}')

Ubuntu با مخزن ondrej یا sury (و سرورهای Forge): همان دستورها، با این تفاوت که Candidate باید نسخه upstream امن را نشان دهد، مثلاً 8.3.36. Forge گزینه -o Dpkg::Options::="--force-confold" را هم پیشنهاد می‌کند تا فایل‌های php.ini و تنظیمات pool شما بازنویسی نشوند. اگر Candidate هنوز قدیمی است، پکیج به mirror نرسیده است. کمی بعد دوباره امتحان کنید و تا آن زمان راهکارهای موقت پایین را اعمال کنید.

Docker:

docker build --pull --no-cache -t myapp:php-8.3.36 .
docker run --rm myapp:php-8.3.36 php -r 'echo PHP_VERSION, PHP_EOL;'

اگر در Dockerfile نسخه را دقیق pin کرده‌اید (مثلاً FROM php:8.3.33-fpm-alpine)، باید خود عدد را عوض کنید. بعد از build، container در حال اجرا را هم با image جدید عوض کنید. build کردن به‌تنهایی چیزی را در production تغییر نمی‌دهد.

هاست اشتراکی: در پنل، نسخه دقیق PHP را ببینید. اگر کمتر از نسخه امن است، از پشتیبانی میزبان بپرسید چه زمانی پچ می‌شود. اگر سایت روی PHP 8.1 یا قدیمی‌تر است، این زمان مناسبی برای ارتقا است.

گام ۴: پردازه‌های قدیمی را restart کنید

پکیج جدید روی دیسک کافی نیست. هر پردازه‌ای که از قبل اجرا شده، باینری قدیمی را در حافظه دارد. ارتقای پکیج معمولاً PHP-FPM را restart می‌کند، اما بررسی کنید. برای Laravel، Forge این دستورها را برای پردازه‌های طولانی پیشنهاد می‌کند:

sudo systemctl restart php8.3-fpm
php artisan queue:restart
php artisan horizon:terminate     # اگر Horizon دارید
php artisan octane:reload         # اگر Octane دارید

queue:restart و horizon:terminate فقط وقتی کار می‌کنند که Supervisor یا systemd workerها را دوباره بالا بیاورد. اگر Octane با FrankenPHP اجرا می‌کنید، خود FrankenPHP درخواست وب می‌گیرد و در فهرست آسیب‌پذیرهاست. پس باید باینری یا image آن را به‌روز کنید، نه فقط worker را reload. برای مدیریت امن workerها هنگام restart، راهنمای رفع خطای job در Horizon هم کمک می‌کند.

گام ۵: تأیید کنید که پچ واقعاً فعال است

php-fpm8.3 -v
dpkg-query -W -f='${Package} ${Version}\n' 'php8.3-*'
ps -o pid,lstart,cmd -C php-fpm8.3 | head -3      # زمان شروع باید بعد از ارتقا باشد

زمان شروع پردازه master در خروجی ps مهم‌ترین نشانه است. اگر قبل از زمان نصب پکیج باشد، FPM هنوز با باینری قدیمی کار می‌کند.

اگر همین الان نمی‌توانید به‌روزرسانی کنید

اطلاعیه php.net چند راهکار موقت می‌دهد. advisory صریحاً می‌گوید تنها راهکار کامل، نصب نسخه امن است. این‌ها فقط ریسک را تا زمان به‌روزرسانی کم می‌کنند:

; در pool مربوط به PHP-FPM، مثلاً /etc/php/8.3/fpm/pool.d/www.conf
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M          ; اگر آپلود دارید حداکثر 32M
php_admin_value[upload_max_filesize] = 2M    ; کمتر از post_max_size
php_admin_flag[session.upload_progress.enabled] = off
; فقط PHP 8.5:
; php_admin_flag[fatal_error_backtraces] = off

بعد از تغییر، همه workerها را restart کنید. reload ممکن است کافی نباشد، چون مشکل دوم در وضعیت باقی‌مانده worker است. به سه نکته توجه کنید. اول، memory_limit=-1 محافظ PHP در برابر مصرف بی‌حد حافظه را برمی‌دارد. php.net توصیه می‌کند محدودیت حافظه را در سطح container یا cgroup بگذارید و اندازه بدنه درخواست را در nginx (client_max_body_size) یا proxy هم محدود کنید. دوم، اگر برنامه شما از نوار پیشرفت آپلود با session استفاده می‌کند، با خاموش‌کردن آن این قابلیت از کار می‌افتد. سوم، تنظیمات deprecated یا نامعتبر در php.ini را که هنگام شروع درخواست هشدار تولید می‌کنند حذف کنید، چون advisory همین هشدارها را یکی از مسیرهای فعال‌کردن باگ دوم می‌داند.

PHP 8.1 و قدیمی‌تر

پروژه PHP برای این نسخه‌ها پچ منتشر نمی‌کند. CISA توصیه کرده سیستم‌هایی که نمی‌توان فوراً به‌روزرسانی کرد یا روی PHP 8.1 و قدیمی‌ترند، تا زمان به‌روزرسانی از دسترس خارج شوند. اگر روی Ubuntu 22.04 با پکیج رسمی php8.1 هستید، Ubuntu پچ backport داده است (نسخه پکیج 8.1.2-1ubuntu2.28). در بقیه موارد، ارتقای نسخه PHP دیگر کار «بعداً» نیست. اگر پروژه شما روی PHP قدیمی‌تر از آن است که بشود سریع ارتقا داد، این دقیقاً موقعیتی است که مهاجرت مرحله‌ای پروژه PHP قدیمی برایش طراحی شده است.

نشانه‌های احتمالی نفوذ

CISA پیشنهاد می‌کند دنبال پردازه‌های PHP بگردید که shell اجرا می‌کنند یا ابزار دانلود می‌کنند. چند بررسی ساده روی سرور Linux:

# فرزندهای غیرعادی FPM (sh، bash، curl، wget زیر php-fpm)
ps -eo pid,ppid,user,cmd --forest | grep -A3 'php-fpm: pool'

# فایل‌های PHP جدید خارج از vendor در چند روز اخیر (deployهای خودتان را کنار بگذارید)
find /var/www -name '*.php' -newermt '2026-10-19' -not -path '*/vendor/*' -not -path '*/storage/framework/*'

# crash workerها در لاگ FPM؛ هر دو باگ می‌توانند worker را از کار بیندازند
grep -E 'SIGSEGV|exited on signal' /var/log/php8.3-fpm.log

خروجی هیچ‌کدام از این‌ها به‌تنهایی اثبات نفوذ یا سلامت نیست. اگر چیز مشکوکی دیدید، سرور را ایزوله کنید، قبل از پاک‌کردن هر چیزی از دیسک و لاگ‌ها نسخه بگیرید و کلیدها و secretهای روی آن سرور را عوض کنید.

چک‌لیست نهایی

  1. فهرست همه سرورها، imageها و هاست‌هایی که PHP اجرا می‌کنند.
  2. نسخه دقیق هر کدام: عدد upstream یا نسخه پکیج توزیع.
  3. ارتقای همه نسخه‌های نصب‌شده، حتی بلااستفاده‌ها.
  4. restart کردن FPM، workerها، Horizon و Octane، و ساخت دوباره imageها با --pull.
  5. تأیید با -v، نسخه پکیج و زمان شروع پردازه.
  6. اگر ارتقا ممکن نیست: راهکارهای موقت php.net و محدودیت حافظه در سطح container.
  7. بررسی نشانه‌های نفوذ و برنامه ارتقا برای PHP 8.1 و قدیمی‌تر.

پچ‌کردن یک آسیب‌پذیری فوری است. نداشتن فهرست سرورها و نسخه‌ها مشکل ساختاری‌تری است که دفعه بعد دوباره تکرار می‌شود. بقیه موارد امنیتی پروژه Laravel را در چک‌لیست امنیت Laravel نوشته‌ام. اگر می‌خواهید سرورها و پروژه‌هایتان بررسی و پچ شوند و فرایند به‌روزرسانی منظم برایشان راه بیفتد، از طریق مشاوره فنی با من در تماس باشید.

منابع

پرسش‌های متداول

آیا Laravel یا WordPress جلوی این حمله را می‌گیرد؟

نه. طبق advisory، روی mod_php حمله پیش از اجرای کد برنامه انجام می‌شود؛ هیچ framework یا افزونه‌ای جایگزین به‌روزرسانی خود PHP نیست.

آیا WAF یا Cloudflare کافی است؟

نه به‌عنوان راه‌حل نهایی. CISA آن را فقط محافظت موقت می‌داند؛ محدودکردن اندازه بدنه درخواست کمک می‌کند، اما تنها راهکار کامل نصب نسخه امن است.