دسته بندی:

امن سازی PHP در سرور

یک اسکریپت آلوده که از راه یک افزونهٔ قدیمی روی سایت آپلود شده، در پیکربندی پیش‌فرض PHP کار زیادی می‌تواند بکند: دستور سیستمی اجرا کند، فایل‌های سایت‌های دیگرِ همان سرور را بخواند، از سرور شما به بیرون درخواست بفرستد و برای خودش در گوشه‌ای یک shell دائمی بسازد. تفاوت میان «یک فایل مخرب آپلود شد» و «کل سرور از دست رفت» را غالباً چند خط در php.ini تعیین می‌کند. امن سازی PHP در سرور دقیقاً یعنی درست گذاشتن همین چند تنظیم؛ سراغشان می‌رویم و در کنارشان تکلیف دو موضوع قدیمی را هم روشن می‌کنیم که هنوز زیاد پرسیده می‌شوند: register_globals و پچ امنیتی suhosin.

اول بفهمید کدام php.ini واقعاً خوانده می‌شود

فایل پیکربندی PHP همان php.ini است و ساده‌ترین راه پیدا کردنش این دستور است:

php --ini

اما یک نکتهٔ مهم که خیلی‌ها را ساعت‌ها سرگردان می‌کند: خروجی این دستور مربوط به SAPI خط فرمان (CLI) است. وب‌سرور شما — چه mod_php باشد، چه PHP-FPM — ممکن است فایل کاملاً دیگری بخواند. اگر تنظیمی را در فایلی که php --ini نشان داده تغییر دادید و در سایت اثری ندید، دلیلش همین است. برای دیدن فایلِ واقعیِ سمت وب، یک فایل با محتوای <?php phpinfo(); بسازید و مقدار Loaded Configuration File را در آن ببینید (و بعد از کار حتماً پاکش کنید)، یا:

php-fpm -i | grep 'Loaded Configuration File'

(روی توزیع‌هایی که چند نسخهٔ PHP دارند نام باینری هم نسخه‌دار است؛ مثلاً php-fpm8.2 -i.) و پیش از دست زدن به هر فایلی یک نسخهٔ پشتیبان بگیرید تا اگر سایت بعد از تغییر بالا نیامد در چند ثانیه بتوانید برگردید:

cp -a /path/to/php.ini /path/to/php.ini.bak-$(date +%F)

روی سرورهای کنترل‌پنلی هر نسخهٔ PHP فایل پیکربندی مستقل خودش را دارد. در cPanel با EasyApache 4 مسیر معمول به شکل /opt/cpanel/ea-phpXX/root/etc/php.ini است و در دایرکت‌ادمین با CustomBuild معمولاً /usr/local/phpXX/lib/php.ini؛ باز هم مقدار دقیق را از خروجی phpinfo() همان دامنه بگیرید، نه از حدس. به بخش Additional .ini files parsed هم نگاه کنید: فایل‌های داخل پوشهٔ conf.d بعد از php.ini خوانده می‌شوند و می‌توانند تنظیم شما را بازنویسی کنند.

تنظیم برای یک سایت، نه برای کل سرور

روی سروری که چند سایت دارد، معمولاً نمی‌خواهید یک تنظیم سخت‌گیرانه همه را از کار بیندازد. سه محل اعمال وجود دارد و هرکدام محدودیت خودش را دارد:

  • pool کانفیگ PHP-FPM (به ازای هر کاربر یا هر سایت یک pool): مطمئن‌ترین جا. تنظیم‌هایی که با php_admin_value و php_admin_flag نوشته شوند از داخل کد قابل تغییر نیستند.
  • فایل .user.ini در ریشهٔ سایت: فقط با SAPI از نوع CGI/FastCGI (یعنی PHP-FPM) کار می‌کند و فقط برای تنظیم‌هایی که سطح دسترسی‌شان PHP_INI_PERDIR یا PHP_INI_USER است. تنظیم‌های سطح سیستم مثل disable_functions از این راه اعمال نمی‌شوند.
  • .htaccess با php_value/php_flag: فقط وقتی PHP به‌صورت ماژول آپاچی (mod_php) اجرا شود. اگر سرور روی PHP-FPM یا CGI باشد، همین یک خط باعث خطای ۵۰۰ می‌شود، چون آپاچی دستور php_value را نمی‌شناسد. این نکته را یادتان باشد؛ پایین‌تر دوباره به آن برمی‌گردیم.

نمونهٔ اعمال در pool کانفیگ PHP-FPM:

php_admin_value[open_basedir] = /home/user/domains/example.com:/tmp
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
php_admin_flag[display_errors] = off

Safe Mode دیگر وجود ندارد

در راهنماهای قدیمی توصیه می‌شد برای جدا کردن سایت‌ها از هم safe_mode = On بگذارید و با safe_mode_gid، safe_mode_include_dir و safe_mode_exec_dir آن را تنظیم کنید. این توصیه امروز کاملاً بی‌اثر است: safe_mode در PHP 5.3 منسوخ و در PHP 5.4 به‌طور کامل حذف شد (نسخه‌ای به نام PHP 6 هم هرگز منتشر نشد). گذاشتن این خط‌ها در php.ini هیچ کاری نمی‌کند و فقط این توهم را می‌سازد که سرور محافظت شده است.

خودِ تیم PHP هم صراحتاً اعلام کرد Safe Mode از پایه اشتباه بود، چون تلاش می‌کرد مسئله‌ای در سطح سیستم‌عامل را در سطح زبان حل کند. جایگزین درست و امروزی‌اش سه چیز است:

  • هر سایت با کاربر یونیکس مستقل اجرا شود و هر کاربر pool مستقل PHP-FPM داشته باشد؛ آنگاه پروسهٔ PHP سایت A اصلاً با مجوز سایت B اجرا نمی‌شود.
  • open_basedir برای محدود کردن دسترسی فایل.
  • مجوزهای درست فایل و پوشه (فایل‌ها ۶۴۴، پوشه‌ها ۷۵۵، و مالکیت درست) به‌جای ۷۷۷ زدن به هر چیزی که خطا داد.

محدود کردن دسترسی فایل با open_basedir

این تنظیم عملگرهای فایلی PHP را به مسیرهای مشخصی محدود می‌کند. اگر مهاجم موفق شود یک اسکریپت روی سایت شما اجرا کند، بدون open_basedir می‌تواند /etc/passwd یا فایل پیکربندی دیتابیس سایت بغلی را بخواند؛ با آن، خواندن هر مسیری بیرون از فهرست مجاز به خطا برمی‌خورد.

open_basedir = /home/user/domains/example.com:/tmp

چند نکتهٔ عملی: جداکنندهٔ مسیرها در لینوکس کاراکتر دو نقطه (:) است؛ اگر مسیر با اسلش پایانی نوشته شود فقط همان دایرکتوری و زیرشاخه‌هایش مجاز است؛ و تقریباً همیشه لازم است مسیر دایرکتوری موقت (چه /tmp چه upload_tmp_dir و session.save_path اختصاصی) هم در فهرست باشد، وگرنه آپلود فایل و session سایت می‌شکند. مطمئن‌ترین جای نوشتن آن، همان pool کانفیگ PHP-FPM است.

غیرفعال کردن توابع خطرناک با disable_functions

مهم‌ترین بخش امن‌سازی PHP همین است. توابعی مثل shell_exec() و system() پلی هستند از کد PHP به خط فرمان سیستم‌عامل؛ اگر مهاجم بتواند یک فایل PHP آپلود کند یا از راه یک آسیب‌پذیری include آن را اجرا کند، این توابع همان چیزی است که با آن shell می‌گیرد. بستن آن‌ها جلوی بخش بزرگی از این حمله‌ها را می‌گیرد.

یک فهرست پایه و امن برای اکثر سرورهای وب:

disable_functions = exec,passthru,shell_exec,system,popen,proc_open,proc_close,proc_get_status,proc_terminate,proc_nice,pcntl_exec,pcntl_fork,pcntl_signal,pcntl_waitpid,dl,show_source,ini_alter,ini_restore,openlog,syslog

موارد اختیاری که بسته به نرم‌افزار سایت ممکن است لازم یا مضر باشند: php_uname، posix_uname، posix_getpwuid، posix_kill، posix_setuid، posix_setgid، posix_setsid، posix_mkfifo و symlink. این‌ها بیشتر برای کاهش افشای اطلاعات سیستم و جلوگیری از حمله‌های symlink مفیدند، ولی بعضی اسکریپت‌ها به آن‌ها متکی‌اند؛ اول روی محیط تست امتحان کنید.

فهرست‌های آمادهٔ اینترنتی را کورکورانه کپی نکنید

فهرست بلندی که سال‌ها در وبلاگ‌ها و انجمن‌های فارسی و انگلیسی دست‌به‌دست شده، چند مورد دارد که نه‌تنها امنیت اضافه نمی‌کند، بلکه سایت را می‌شکند:

  • escapeshellarg و escapeshellcmd دقیقاً توابع ضدعفونی‌کنندهٔ ورودی هستند، نه اجراکنندهٔ دستور. بستن آن‌ها کدی را که با احتیاط نوشته شده از کار می‌اندازد و کدهای بی‌احتیاط را دست‌نخورده باقی می‌گذارد.
  • set_time_limit، tmpfile، tempnam، readlink، getcwd، stream_select و خانوادهٔ socket_* در کارهای کاملاً عادی استفاده می‌شوند؛ بستن آن‌ها وردپرس، لاراول، ارسال ایمیل و بک‌آپ‌گیری را خراب می‌کند. (در آن فهرست حتی detcwd نوشته شده بود که اصلاً تابعی به این نام وجود ندارد و غلط تایپی getcwd است.)
  • pfsockopen (و برادرش fsockopen) در آن فهرست‌ها زیاد دیده می‌شود، ولی همان چیزی است که کتابخانه‌های ارسال ایمیل مثل PHPMailer برای اتصال SMTP از آن استفاده می‌کنند؛ با بستنش ایمیل سایت بی‌صدا قطع می‌شود.
  • mysql_list_dbs و بقیهٔ توابع mysql_* در PHP 7 حذف شده‌اند؛ نوشتنشان در فهرست فقط آن را شلوغ می‌کند.

روی دایرکت‌ادمین لازم نیست این کار را دستی کنید؛ CustomBuild یک ابزار آماده دارد که یک فهرست محافظه‌کارانه را روی php.ini همهٔ نسخه‌های نصب‌شده اعمال می‌کند (و در همان مسیر expose_php را هم Off می‌کند):

da build secure_php

دو نکته که معمولاً از قلم می‌افتد

یک: فایل php.ini خط فرمان جداست. اگر disable_functions را روی نسخهٔ CLI هم اعمال کنید، ابزارهایی مثل Composer، WP-CLI و کران‌جاب‌های خودتان (که به proc_open نیاز دارند) از کار می‌افتند. سخت‌گیری را روی SAPI وب اعمال کنید و CLI را قابل استفاده نگه دارید.

دو: یک روش شناخته‌شده برای دور زدن disable_functions وجود دارد که با ترکیب putenv() و تابع mail() کار می‌کند (بارگذاری یک کتابخانهٔ دلخواه هنگام اجرای sendmail). اگر سایت شما ایمیل را از راه SMTP می‌فرستد و به تابع داخلی mail() نیازی ندارد، اضافه کردن mail به فهرست، این مسیر را می‌بندد. خودِ putenv را با احتیاط ببندید، چون بعضی فریم‌ورک‌ها از آن استفاده می‌کنند.

و مهم‌تر از همه: disable_functions یک لایه است، نه حصار نفوذناپذیر. بدون به‌روز نگه‌داشتن PHP، وردپرس، افزونه‌ها و قالب‌ها، این فهرست شما را نجات نمی‌دهد.

جلوگیری از افشای اطلاعات

مهاجم پیش از حمله به شناسایی نیاز دارد و سخاوتمندترین منبع اطلاعات، پیام‌های خطای خود سایت است: مسیر نصب برنامه، نام کاربری دیتابیس، نام جدول و ستون، و حتی محتوای متغیرها. روی سرور تولیدی خطاها باید در فایل لاگ ثبت شوند، نه در صفحهٔ کاربر:

display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/log/php/error.log
error_reporting = E_ALL
expose_php = Off

خط آخر هم هدر X-Powered-By را حذف می‌کند تا نسخهٔ دقیق PHP شما در پاسخ HTTP به همه اعلام نشود. دقت کنید که دایرکتوری فایل لاگ باید وجود داشته باشد و کاربر PHP اجازهٔ نوشتن در آن را داشته باشد، وگرنه خطاها هیچ‌جا ثبت نمی‌شوند.

register_globals: حذف شده و نباید برگردد

این پرسش هنوز پرتکرار است: «چطور register_globals را روشن کنم؟» — معمولاً وقتی کسی می‌خواهد یک CMS یا اسکریپت قدیمی را روی سرور جدید بالا بیاورد و با انبوه خطای «متغیر تعریف‌نشده» روبه‌رو می‌شود.

پاسخ کوتاه: نمی‌شود روشنش کرد. register_globals در PHP 5.3 منسوخ و در PHP 5.4 حذف شد. این قابلیت باعث می‌شد هر پارامتر GET، POST یا کوکی به‌طور خودکار به یک متغیر معمولی PHP تبدیل شود؛ یعنی مهاجم فقط با اضافه کردن ?admin=1 به آدرس، می‌توانست متغیری را در دل اسکریپت مقداردهی کند که برنامه‌نویس فرض کرده بود هرگز از بیرون قابل تعیین نیست. دقیقاً به همین دلیل حذف شد. magic_quotes_gpc و آرایه‌های قدیمی مثل $HTTP_POST_VARS هم در همان نسخه حذف شدند و مشکل سایت‌های قدیمی معمولاً ترکیبی از هر سه است.

دستوری که در راهنماهای قدیمی برای فعال کردن آن معرفی می‌شد، امروز دو نتیجه بیشتر ندارد:

php_value register_globals 1

اگر PHP به‌صورت mod_php اجرا شود، این خط در .htaccess صرفاً نادیده گرفته می‌شود چون چنین تنظیمی دیگر در PHP وجود ندارد. و اگر سرور روی PHP-FPM یا CGI باشد — که پیکربندی رایج امروز است — آپاچی دستور php_value را نمی‌شناسد و کل سایت با خطای 500 Internal Server Error از کار می‌افتد. یعنی همان خطی که قرار بود مشکل را حل کند، سایت را کاملاً پایین می‌آورد.

پس با اسکریپت قدیمی چه کنیم؟

راه درست، اصلاح کد است و معمولاً هم آن‌قدرها که به‌نظر می‌رسد سخت نیست: هر جا متغیری مثل $username مستقیم از ورودی کاربر می‌آید، آن را به $_GET['username'] یا $_POST['username'] تبدیل کنید و مقدارش را اعتبارسنجی کنید. همین کار، هم سایت را روی PHP جدید اجرا می‌کند و هم آسیب‌پذیری اصلی را می‌بندد.

اگر ناچارید تا زمان بازنویسی، اسکریپت را موقتاً سرِ پا نگه دارید، می‌توان رفتار قدیمی را در ابتدای فایل ورودی برنامه شبیه‌سازی کرد:

<?php
// هشدار: این کد همان آسیب‌پذیری register_globals را برمی‌گرداند
extract($_REQUEST, EXTR_SKIP);

این کار را فقط روی سایتی انجام دهید که در دسترس عموم نیست (محیط تست یا پشت محدودیت IP) و آن را راه‌حل ندانید؛ راه‌حل، مهاجرت به کدی است که هنوز پشتیبانی می‌شود. اجرای دائمی یک CMS قدیمی روی نسخهٔ منسوخ PHP فقط برای اینکه register_globals داشته باشد، به‌مراتب خطرناک‌تر از خود register_globals است، چون نسخه‌های PHP منسوخ دیگر وصلهٔ امنیتی نمی‌گیرند.

بستن include از راه دور

حملهٔ RFI (Remote File Inclusion) وقتی ممکن می‌شود که اسکریپت، آدرسی را از ورودی کاربر بگیرد و آن را include کند؛ مهاجم آدرس فایل PHP خودش را می‌دهد و کدش روی سرور شما اجرا می‌شود. تنظیم مربوطه:

allow_url_include = Off
allow_url_fopen = Off

خط اول را بی‌قید و شرط نگه دارید؛ خوشبختانه از PHP 5.2 به بعد مقدار پیش‌فرضش هم Off است و بدون allow_url_fopen اصلاً کار نمی‌کند. اما خط دوم را با فکر تغییر دهید: بستن allow_url_fopen باعث می‌شود file_get_contents('https://…') و هر کتابخانه‌ای که با stream wrapper به بیرون درخواست می‌فرستد از کار بیفتد. اگر سایت شما به فراخوانی سرویس بیرونی نیاز دارد، راه درست این است که allow_url_fopen بسته بماند و کد از cURL استفاده کند؛ اگر نمی‌توانید کد را عوض کنید، دست‌کم allow_url_include را حتماً Off نگه دارید.

آپلود فایل را مهار کنید

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

file_uploads = Off

و اگر آپلود لازم است، این سه تنظیم را درست بگذارید (توجه کنید که نام صحیح تنظیم upload_max_filesize است؛ املای upload_max_filezize که در راهنماهای زیادی تکرار شده غلط است و PHP آن را نادیده می‌گیرد):

upload_max_filesize = 2M
post_max_size = 8M
upload_tmp_dir = /var/php_tmp

مقدار post_max_size باید از upload_max_filesize بزرگ‌تر یا مساوی باشد، وگرنه سقف واقعی آپلود همان post_max_size خواهد بود. دایرکتوری upload_tmp_dir هم باید وجود داشته باشد، مالکیتش با کاربر PHP باشد، بیرون از ریشهٔ سایت قرار بگیرد و حتماً در فهرست open_basedir باشد.

اجرای PHP را در پوشهٔ آپلود ببندید

مهم‌ترین کار در این حوزه اصلاً در php.ini نیست: پوشه‌ای که فایل‌های کاربران در آن می‌نشیند نباید بتواند PHP اجرا کند. در آپاچی ۲.۴ (نسخهٔ امروزی):

<Directory /home/user/domains/example.com/public_html/wp-content/uploads>
    <FilesMatch ".ph(p[0-9]?|tml|ar)$">
        Require all denied
    </FilesMatch>
</Directory>

دو نکتهٔ اجرایی دربارهٔ همین بلوک: بلوک <Directory> فقط در کانفیگ سرور یا vhost معتبر است و در .htaccess کار نمی‌کند (در .htaccess فقط بخش <FilesMatch> داخلی را بگذارید). و روی سرورهای کنترل‌پنلی آن را در فایل کانفیگ سفارشیِ خود پنل بنویسید، نه مستقیم در vhost تولیدشده؛ وگرنه اولین بار که پنل کانفیگ‌ها را بازنویسی کند (در دایرکت‌ادمین da build rewrite_confs) تغییر شما پاک می‌شود.

اگر هنوز روی آپاچی ۲.۲ هستید، ساختار دستور فرق می‌کند و به‌جای Require باید از سبک قدیمی استفاده کنید. این نسخه از آپاچی سال‌هاست پشتیبانی نمی‌شود و بهتر است ارتقا دهید، ولی برای سرورهای قدیمی معادلش این است:

<FilesMatch ".ph(p[0-9]?|tml|ar)$">
    Order allow,deny
    Deny from all
</FilesMatch>

و در Nginx:

location ~* /uploads/.*.php$ {
    deny all;
}

در Nginx ترتیب مهم است: بلوک بالا باید پیش از بلوک location ~ .php$ که درخواست‌ها را به PHP-FPM می‌فرستد نوشته شود، چون اولین location منطبق برنده است. هر تغییری در کانفیگ را هم قبل از ری‌استارت با nginx -t (و در آپاچی با apachectl configtest) اعتبارسنجی کنید؛ یک پرانتز جامانده کافی است تا وب‌سرور دیگر بالا نیاید.

محافظت از session

ربودن session (session hijacking) حملهٔ محبوبی است: مهاجم شناسهٔ session کاربر معتبر را به‌دست می‌آورد و بدون دانستن رمز عبور، با دسترسی او وارد بخش‌های محافظت‌شده می‌شود. تنظیم‌های امروزی برای سخت کردن این کار:

session.save_path = /var/lib/php/session
session.use_strict_mode = 1
session.use_only_cookies = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax

توضیح کوتاه هرکدام: use_strict_mode باعث می‌شود PHP شناسهٔ session ساختهٔ خود مرورگر را نپذیرد و جلوی حملهٔ session fixation را می‌گیرد. cookie_httponly کوکی را از دسترس جاوااسکریپت خارج می‌کند تا یک آسیب‌پذیری XSS به سرقت session تبدیل نشود. cookie_secure کوکی را فقط روی HTTPS می‌فرستد (اگر سایت هنوز کاملاً روی HTTPS نیست، این را روشن نکنید تا ورود کاربران قطع نشود). cookie_samesite از PHP 7.3 اضافه شده و کوکی را در درخواست‌های بین‌سایتی محدود می‌کند.

دربارهٔ session.save_path: مسیر پیش‌فرض روی سرور اشتراکی مشکل جدی دارد، چون همهٔ سایت‌ها فایل‌های session‌شان را در یک پوشهٔ مشترک می‌نویسند و یک سایت آلوده می‌تواند session‌های سایت‌های دیگر را بخواند. برای هر سایت یک پوشهٔ اختصاصی بسازید، بیرون از ریشهٔ سند، با مالکیت کاربر همان سایت و دسترسی 0700، و آن را در open_basedir بیاورید.

یک نکتهٔ اصلاحی هم دربارهٔ session.referer_check که در راهنماهای قدیمی توصیه می‌شد: این تنظیم هنوز وجود دارد، اما تکیه‌کردنی نیست. هدر Referer را مرورگر می‌فرستد، بسیاری از مرورگرها و افزونه‌های حریم خصوصی آن را حذف می‌کنند و مهاجم به‌راحتی می‌تواند جعلش کند. نتیجه‌اش معمولاً قطع شدن session کاربران واقعی است، نه جلوگیری از حمله. به‌جایش روی چهار تنظیم بالا و بازتولید شناسهٔ session بعد از ورود کاربر (session_regenerate_id(true)) حساب کنید.

پچ امنیتی suhosin و سرورهای دایرکت‌ادمین

suhosin یک لایهٔ حفاظتی برای PHP بود که در سطح خودِ مفسر کار می‌کرد: محدودیت روی طول و تعداد متغیرهای ورودی، جلوگیری از برخی الگوهای شناخته‌شدهٔ حمله و لاگ‌کردن رفتارهای مشکوک. سال‌ها یکی از توصیه‌های استاندارد برای سخت‌سازی سرورهای هاستینگ بود؛ در cPanel از راه EasyApache نصب می‌شد و در دایرکت‌ادمین از راه CustomBuild.

اما قبل از هر کاری وضعیت امروزش را بدانید: توسعهٔ suhosin در دورهٔ PHP 5 متوقف شد و شاخهٔ suhosin7 (برای PHP 7) هیچ‌وقت به نسخهٔ پایدار و نگهداری‌شده نرسید. به همین دلیل خودِ دایرکت‌ادمین گزینهٔ suhosin را از CustomBuild برداشت و به‌جایش Snuffleupagus را اضافه کرد که برای PHP 7 و ۸ نوشته شده و فعالانه نگهداری می‌شود. یعنی روی یک سرور دایرکت‌ادمینِ به‌روز، عوض کردن suhosin=no در options.conf اصلاً کاری نمی‌کند، چون چنین گزینه‌ای دیگر در آن فایل نیست. اول ببینید روی سرور خودتان چه چیزی واقعاً در دسترس است:

cd /usr/local/directadmin/custombuild
./build options
./build versions

اگر روی یک سرور قدیمی هستید که گزینهٔ suhosin هنوز در options.conf آن وجود دارد، روش تاریخی‌اش این بود: مقدار suhosin=no را به suhosin=yes تغییر دهید (به‌جای ویرایش دستی فایل، بهتر است از خودِ CustomBuild استفاده کنید) و PHP را دوباره build کنید:

cd /usr/local/directadmin/custombuild
./build update
./build set suhosin yes
./build php n
./build rewrite_confs

در نسخه‌های جدیدتر دایرکت‌ادمین همین دستورها با پوستهٔ da build هم اجرا می‌شوند (مثلاً da build php n). دو هشدار عملی: build دوبارهٔ PHP روی یک سرور زنده چند ده دقیقه طول می‌کشد و در بخشی از آن سایت‌ها خطا می‌دهند، پس آن را در ساعت کم‌ترافیک و با اسنپ‌شات/بکاپ قبلی انجام دهید؛ و اگر PHP در حالت mod_php اجرا شود، CustomBuild این پچ را حتی با suhosin=yes هم نصب نمی‌کرد.

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

پس امروز به‌جای suhosin چه کار کنیم؟

عیب همیشگی suhosin سر جایش است: با بعضی اپلیکیشن‌ها، کتابخانه‌ها و اکستنشن‌های PHP سازگار نیست و می‌تواند بی‌سروصدا باعث خطا در فرم‌های بزرگ یا اسکریپت‌های خاص شود. حالا مسئلهٔ نگهداری هم به آن اضافه شده؛ و اکستنشنی که نگهداری نمی‌شود خودش می‌تواند به نقطهٔ ضعف تبدیل شود. روی نسخه‌های امروزی PHP، همان محافظتی را که از suhosin انتظار داشتید از این ترکیب بگیرید: PHP به‌روز و در بازهٔ پشتیبانی رسمی، همان تنظیم‌های php.ini که در این مقاله آمد، جداسازی کاربران با pool مستقل PHP-FPM، یک WAF مانند ModSecurity روی وب‌سرور، و اگر واقعاً به سخت‌سازی در سطح مفسر نیاز دارید، Snuffleupagus که جانشین رسمی این نقش در CustomBuild است.

اعمال تغییرات و بررسی نتیجه

تغییر php.ini تا وقتی سرویس مربوطه ری‌استارت نشود اثری ندارد. اما اول تست کنید، بعد ری‌استارت؛ یک خط اشتباه در پیکربندی باعث می‌شود سرویس دیگر بالا نیاید و سایت به‌جای «سخت‌تر»، «خاموش» شود:

php-fpm -t          # صحت کانفیگ PHP-FPM
apachectl configtest # آپاچی
nginx -t             # انجینکس

(طبیعتاً فقط دستور وب‌سروری را که واقعاً روی سرور شما نصب است اجرا کنید.) وقتی پاسخ سالم گرفتید، سرویس را ری‌استارت کنید. دستورهای قدیمی service php-fpm restart و /etc/init.d/httpd restart جای خود را به systemd داده‌اند:

# AlmaLinux / Rocky Linux / CentOS 7 به بعد
systemctl restart php-fpm httpd

# Ubuntu / Debian (شماره نسخه را با نسخه خودتان جایگزین کنید)
systemctl restart php8.2-fpm apache2

روی سرورهای کنترل‌پنلی که چند نسخهٔ PHP نصب است، نام سرویس‌ها متفاوت است؛ به‌جای حدس زدن، فهرست را بگیرید:

systemctl list-units --type=service | grep -i php

بعد از ری‌استارت، نتیجه را هم برای وب و هم برای CLI بررسی کنید:

php -i | grep -E 'disable_functions|open_basedir|display_errors'

و برای سمت وب، دوباره یک صفحهٔ phpinfo() موقت بسازید و ستون Local Value را ببینید — این ستون است که مقدار واقعی اعمال‌شده روی آن سایت را نشان می‌دهد. این فایل را با نامی غیرقابل‌حدس بسازید و بلافاصله بعد از خواندن پاکش کنید؛ یک info.php جامانده دقیقاً همان نقشهٔ راهی است که در بخش «افشای اطلاعات» سعی کردیم از دست مهاجم بگیریم. در پایان، مهم‌ترین آزمون این است: خود سایت را باز کنید، یک بار وارد پنل مدیریتش شوید، فرم آپلود و ارسال ایمیلش را امتحان کنید و لاگ خطا را نگاه کنید. بیشتر خرابی‌های ناشی از سخت‌سازی PHP بلافاصله در همین سه کار خودشان را نشان می‌دهند.

اگر روی هاست اشتراکی هستید، بخش عمدهٔ این تنظیم‌ها را مدیر سرور اعمال کرده و از حساب کاربری قابل تغییر نیستند؛ در این حالت سراغ چیزهایی بروید که در اختیار شماست: به‌روز نگه‌داشتن CMS و افزونه‌ها، حذف قالب‌ها و افزونه‌های بلااستفاده، و بستن اجرای PHP در پوشهٔ آپلود. و اگر مدیریت سرور اختصاصی یا مجازی را خودتان بر عهده ندارید، اعمال این تغییرات کاری است که تیم سرور مجازی مدیریت‌شده باید برایتان انجام دهد.