PHP2Shell на Windows Server: CVE-2026-109618 и CVE-2026-109619 в IIS и FastCGI
На 21 октября 2026 исправлены версии PHP 8.5.12, 8.4.27, 8.3.36 и 8.2.35, для 8.1 и старше исправления нет. На Windows сначала найдите все копии php-cgi.exe и php.exe, проверьте версию, затем обновите сборку вручную и перезапустите пул. Наличие этих сборок на windows.php.net проверьте сами, подтверждения мы не получили. Пока обновить нельзя, закройте multipart/form-data в IIS и отключите session.upload_progress.
Что известно на 21 октября 2026 и чем Windows отличается от Linux
21 октября 2026 года PHP выпустил исправления для двух критических уязвимостей ядра, получивших общее название PHP2Shell: CVE-2026-109618 и CVE-2026-109619. Обе оценены в 9,2 балла по CVSS v4.0, обе используются без аутентификации по сети, а конфигурация по умолчанию уязвима. Исправленные версии upstream: 8.5.12, 8.4.27, 8.3.36 и 8.2.35. Для PHP 8.1 и старше официального исправления нет, ветки закрыты. Подробный разбор уязвимости и план первого часа мы вынесли в статью PHP2Shell: что делать за первый час, здесь только то, что относится к Windows Server.
Главное отличие Windows в том, что у вас нет менеджера пакетов, который сам подтянет исправление. На Debian или Ubuntu достаточно apt, на Windows PHP — это папка с архивом, которую когда-то распаковал администратор, и в таком виде PHP не обновляется ни вместе с системой, ни через Windows Update. Вторая особенность: интерпретатор на одном сервере часто лежит в нескольких местах сразу. Один PHP стоит под IIS, второй приехал с XAMPP или OpenServer, третий спрятан внутри веб-окружения какого-то продукта, четвёртый остался от давно забытого теста. Уязвимо любое из этих мест, если оно принимает запросы из сети.
На 21 октября мы не нашли подтверждения, что исправленные Windows-сборки уже лежат на windows.php.net. Страницу загрузки из нашей среды открыть не удалось, а поисковая выдача показывала устаревшие снимки. Поэтому мы не утверждаем, что там есть 8.5.12, 8.4.27, 8.3.36 или 8.2.35, и просим вас не полагаться на нас: откройте https://windows.php.net/download/ и сверьте номер версии в имени архива. Если нужного номера там пока нет, действуйте по разделу о временных мерах, а страницу проверяйте повторно. Заявлений вендоров XAMPP и OpenServer про исправленные сборки на момент проверки мы тоже не нашли, смотрите их официальные сайты.
Порядок работ у нас такой: найти все копии PHP, определить версию и пул, который ими пользуется, обновить сборку вручную, перезапустить и проверить, а если обновить сегодня нельзя — закрыть самые опасные пути временными мерами. Временные меры частичные: специалист PHP Джина Питер Банъярд прямо говорит, что надёжных обходных путей нет и единственное исправление — патч. Учтите это при планировании сроков.
- Исправленные версии upstream на 21.10.2026: 8.5.12, 8.4.27, 8.3.36, 8.2.35
- PHP 8.1 и старше: официального исправления нет
- Наличие этих сборок на windows.php.net проверяйте на самой странице загрузки
- Режимы обслуживания CMS не помогают: PHP разбирает multipart-запрос раньше кода приложения
Шаг 1. Находим все php-cgi.exe и php.exe на сервере
Начинаем с инвентаризации, как рекомендует и британский NCSC: пока вы не знаете, где лежат все копии PHP, обновление будет неполным. Нужны три источника данных: файлы на дисках, настройки IIS и запущенные процессы. Файлы покажут всё, что лежит на диске, настройки IIS покажут, какая из копий реально привязана к сайтам, процессы покажут, что работает прямо сейчас. Расхождение между этими списками и есть самые интересные находки.
Сначала ищем файлы на всех локальных дисках. Нас интересуют не только php-cgi.exe и php.exe, но и библиотеки интерпретатора: php8ts.dll у Thread Safe-сборок, php8.dll у Non Thread Safe и php8apache2_4.dll у модуля для Apache. По библиотекам видно сборки, у которых исполняемого файла нет или он переименован. Команду запускайте в PowerShell от администратора, поиск по большим дискам занимает несколько минут.
$names = 'php.exe','php-cgi.exe','php-win.exe','php7ts.dll','php7.dll','php8ts.dll','php8.dll','php8apache2_4.dll'
Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object {
Get-ChildItem -Path ($_.DeviceID + '\') -Recurse -Force -Include $names -ErrorAction SilentlyContinue
} | Select-Object FullName, LastWriteTime, @{n='Version';e={$_.VersionInfo.ProductVersion}} |
Sort-Object FullName | Format-Table -AutoSize -WrapСтолбец Version берётся из свойств файла и годится как первое приближение, а окончательным ответом остаётся вывод php.exe -v из следующего шага. Если в списке больше одной папки — это нормально и именно ради этого мы начинаем с поиска. Отдельно проверьте, какой php.exe находится первым в PATH, потому что скрипты планировщика заданий нередко вызывают его без полного пути.
where.exe php
where.exe php-cgi
Get-CimInstance Win32_Process -Filter "Name='php-cgi.exe' OR Name='php.exe' OR Name='httpd.exe'" |
Select-Object ProcessId, ParentProcessId, ExecutablePath, CreationDateТеперь смотрим, какие копии PHP привязаны к IIS. Сопоставление расширения .php с обработчиком хранится в разделе system.webServer/handlers, а параметры самих процессов — в system.webServer/fastCgi. Через модуль WebAdministration можно пройти по всем сайтам и вывести обработчики FastCGI вместе с путём к исполняемому файлу. Строки будут повторяться для сайтов, которые наследуют обработчик с уровня сервера, это ожидаемо. Важно другое: у разных сайтов в поле ScriptProcessor могут стоять разные пути.
Import-Module WebAdministration
Get-Website | ForEach-Object {
Get-WebConfiguration -Filter "system.webServer/handlers/add[@modules='FastCgiModule']" -PSPath ("IIS:\Sites\" + $_.Name)
} | Select-Object PSPath, Name, Path, ScriptProcessor | Sort-Object ScriptProcessor, PSPath -Unique
Get-WebConfiguration -Filter 'system.webServer/fastCgi/application' -PSPath 'MACHINE/WEBROOT/APPHOST' |
Select-Object fullPath, arguments, maxInstances, instanceMaxRequestsТо же самое делает appcmd.exe, который лежит в каталоге %windir%\system32\inetsrv. Удобно, когда модуль WebAdministration недоступен или нужен текстовый вывод для отчёта владельцу. В выводе ищите строки с php в scriptProcessor и fullPath.
%windir%\system32\inetsrv\appcmd.exe list config -section:system.webServer/fastCgi
%windir%\system32\inetsrv\appcmd.exe list config -section:system.webServer/handlers | findstr /i php
%windir%\system32\inetsrv\appcmd.exe list apppoolПоследний источник — службы Windows и планировщик. Apache из XAMPP или Apache Lounge, nginx с php-cgi и службы, созданные через NSSM, не видны в настройках IIS. Проверьте службы с httpd, php, nginx в пути запуска и задания планировщика, которые вызывают php.exe. CLI-вызовы по расписанию не принимают multipart-запросы из сети, но обновить их стоит вместе с остальными, чтобы на сервере не осталось разных версий.
Get-CimInstance Win32_Service | Where-Object { $_.PathName -match 'httpd|php|nginx|apache' } |
Select-Object Name, State, StartMode, PathName
Get-ScheduledTask | Where-Object { $_.Actions.Execute -match 'php' } |
Select-Object TaskName, TaskPath, @{n='Cmd';e={$_.Actions.Execute + ' ' + $_.Actions.Arguments}}Результат сводим в таблицу: путь, версия, кто использует, доступен ли из сети. Этот список пригодится дважды: для плана обновления и для проверки после него. Если вы строите общий реестр ИТ-активов компании, добавьте туда PHP как отдельную позицию; о том, как вести такой учёт, мы писали в материале про инвентаризацию ИТ-активов.
Шаг 2. Узнаём версию и сверяем с исправленными номерами
Для каждой найденной копии запускаем интерпретатор с ключом -v. Для php-cgi.exe тоже работает -v. Заодно выводим путь к php.ini, который реально загрузится, и список расширений. Это важно, потому что на Windows php.ini ищется в нескольких местах: рядом с исполняемым файлом, по переменной окружения PHPRC, в каталоге Windows. Правите вы один файл, а работает другой — типичная причина «мы же отключили, а оно включено».
& 'C:\php\php.exe' -v
& 'C:\php\php.exe' --ini
& 'C:\php\php.exe' -m
& 'C:\php\php.exe' -r "echo PHP_VERSION, ' ', PHP_ZTS ? 'TS' : 'NTS', ' ', PHP_INT_SIZE*8, 'bit', PHP_EOL;"
& 'C:\php\php.exe' -i | Select-String 'upload_progress.enabled|memory_limit|post_max_size|Loaded Configuration File'Константа PHP_ZTS покажет тип сборки: 1 у Thread Safe и 0 у Non Thread Safe. Сравниваем номер версии с таблицей. Проверка идёт строго по номеру версии, потому что в Windows-сборках нет changelog пакета, где можно было бы найти идентификатор CVE.
| Ветка PHP | Исправленная версия upstream на 21.10.2026 | Что делать на Windows |
|---|---|---|
| 8.5 | 8.5.12 | Обновить сборку, если номер ниже |
| 8.4 | 8.4.27 | Обновить сборку, если номер ниже |
| 8.3 | 8.3.36 | Обновить сборку, если номер ниже |
| 8.2 | 8.2.35 | Обновить сборку, если номер ниже |
| 8.1 и старше | Официального исправления нет | Планировать переход на 8.2 или новее, до тех пор применять временные меры |
Номер ниже указанного в таблице означает, что копия уязвима. Номер равен или выше означает, что исправление в сборке есть. Если в вашей ветке нужный номер на windows.php.net не нашёлся, не делайте вывод, что «значит, мы в безопасности» или «значит, нужно ставить что попало». Запишите это как открытый пункт, переходите к временным мерам и проверяйте страницу каждый день.
Отдельная история с PHP 8.1 и старше. Patchstack пишет, что минимальные бэкпорты для 7.4, 8.0 и 8.1 существуют только при сборке из исходников. На Windows это означает самостоятельную сборку с Visual Studio, и для компании нашего масштаба это почти никогда не оправдано. Разумный путь: проверить, поддерживает ли ваше приложение 8.2 и новее, провести обновление на тестовой копии и перейти. До перехода на такой копии держите временные меры из шестого раздела и ограничьте доступ к сайту по возможности.
Если у вас Apache на Windows, то версию PHP определяет не php.exe рядом, а библиотека, которую загружает Apache директивой LoadModule php_module. Откройте httpd.conf (или файл, который он подключает) и найдите эту строку: путь в ней покажет, какую сборку использует веб-сервер. Дальше действуем как с любой копией: запускаем php.exe -v из того же каталога.
Шаг 3. Обновляем сборку с windows.php.net вручную
Web Platform Installer для этого не годится. Microsoft сообщила о его выводе из эксплуатации: поддержка продукта закончилась 1 июля 2022 года, а на странице загрузки указано, что он снят 31 декабря 2022 года. Каталог приложений в нём не обновляется с 2021 года. Поэтому ставьте и обновляйте PHP вручную: скачали архив, распаковали, перенесли конфигурацию, проверили. Так работает и официальная инструкция PHP для Windows, и так делаем мы у клиентов. Модули IIS вроде URL Rewrite тоже устанавливаются отдельными msi-пакетами с официальных страниц IIS.
Для IIS с FastCGI берут Non Thread Safe (NTS) сборку. FastCGI запускает каждый запрос в отдельном процессе, потокобезопасность не нужна, а NTS работает быстрее. Thread Safe (TS) нужен, когда PHP подключается к Apache как модуль или встроен в многопоточное приложение. Менять тип сборки при обновлении нельзя: расширения собираются под конкретный тип, и DLL от TS не загрузится в NTS, и наоборот. Поэтому сначала запомните результат PHP_ZTS из прошлого шага и скачивайте тот же вариант. Разрядность тоже должна остаться прежней, в большинстве случаев это x64.
В имени архива указана версия компилятора, например vs16 или vs17. Для неё нужен соответствующий Microsoft Visual C++ Redistributable, который ставится отдельно с сайта Microsoft; требуемую версию и разрядность вендор указывает на странице загрузки рядом со сборкой. Без него php-cgi.exe не запустится, а IIS ответит ошибкой 500 с кодом, ссылающимся на отсутствующую DLL. Проверяйте это до остановки сайта, а не после. Если на странице есть контрольные суммы, сверьте SHA256 скачанного архива. Скачивайте архив только со страницы windows.php.net, а не со сторонних зеркал.
Дальше порядок такой. Распаковываем новую сборку в отдельную папку рядом со старой и проверяем её из командной строки, не трогая работающий сайт. Затем в окно обслуживания останавливаем пул, переименовываем старый каталог в архивный и ставим новый на его место. Путь оставляем прежним: в настройках IIS путь к php-cgi.exe прописан дважды, в обработчике и в разделе fastCgi, и значения должны совпадать. Если путь меняется, правьте оба места.
# 1. Распаковка новой сборки рядом. Имя архива подставьте своё: php-<версия>-nts-Win32-<vsNN>-x64.zip
Expand-Archive -Path C:\Distr\php-NEW.zip -DestinationPath C:\php_new
# 2. Перенос конфигурации. Берём php.ini-production новой сборки и сравниваем с рабочим файлом
Compare-Object (Get-Content C:\php\php.ini) (Get-Content C:\php_new\php.ini-production) | Select-Object -First 80
# 3. Проверка новой сборки без трафика
& C:\php_new\php.exe -v
& C:\php_new\php.exe -mФайл php.ini новой сборки не копируйте поверх чужого вслепую. В каждой ветке меняются значения по умолчанию и список директив, а перенос старого файла в новую версию иногда включает директивы, которых больше нет. Берём php.ini-production из новой сборки, переносим туда осознанные настройки старого файла: extension_dir, список расширений, часовой пояс, error_log, лимиты загрузки, fastcgi.impersonate, cgi.fix_pathinfo и параметры сессий. Всё остальное оставляем по умолчанию. Для расширений действует жёсткое правило: используем только DLL из каталога ext новой сборки. Старые DLL расширений от другой ветки PHP не загрузятся и не должны оказываться в новом каталоге.
Отдельное предупреждение про сторонние расширения. Расширение WinCache собирается под конкретную ветку PHP, и вариант под вашу версию нужно искать отдельно: на 21 октября мы не проверяли, есть ли сборка WinCache под актуальные ветки 8.x, и не утверждаем этого. OPcache встроен в PHP и подключается директивой zend_extension=opcache, отдельной DLL из архива не нужно. Если ваш сайт использовал кэш WinCache и под новую ветку его нет, выключайте его, а не копируйте старую DLL. Источники называют OPcache участником одной из уязвимостей, но отключение OPcache как меры защиты ни в одном из них не названо, поэтому мерой его не считайте.
Import-Module WebAdministration
Stop-WebAppPool -Name 'DefaultAppPool'
Get-Process php-cgi -ErrorAction SilentlyContinue # должно быть пусто, иначе файлы заняты
Rename-Item C:\php C:\php_old_$(Get-Date -Format yyyyMMdd)
Rename-Item C:\php_new C:\php
Copy-Item C:\php_old_*\php.ini C:\php\php.ini -Confirm # только если вы уже сверили его с php.ini-production
& C:\php\php.exe -v
Start-WebAppPool -Name 'DefaultAppPool'Название пула подставьте своё, список даёт appcmd list apppool. Если после остановки пула процессы php-cgi.exe остались, остановите службу IIS целиком командой iisreset /stop, выполните замену и запустите iisreset /start. Старый каталог не удаляйте несколько дней: он нужен для отката. Но следите, чтобы он не оставался доступным для запуска: переименованная папка с рабочим php-cgi.exe остаётся такой же уязвимой копией, как любая другая. Когда убедитесь, что откат не нужен, убирайте её из системы или хотя бы переносите архив на другой диск.
Шаг 4. Перезапуск, настройки FastCGI и проверка результата
После замены файлов важно убедиться, что запущены именно новые процессы. Старый php-cgi.exe, поднятый до обновления, продолжает работать с уязвимым кодом в памяти, даже если файл на диске уже новый. Поэтому перезапуск обязателен: пула приложений достаточно для IIS, Apache требует именно restart службы. Для mod_php в источниках отдельно сказано, что reload и graceful недостаточны, мы переносим это правило и на Windows-Apache.
| Стек | Что перезапустить | Команда |
|---|---|---|
| IIS и FastCGI | Пул приложений сайта | Restart-WebAppPool -Name "имя_пула" или appcmd recycle apppool /apppool.name:"имя_пула" |
| IIS, файлы заняты или есть сомнения | Служба IIS целиком | iisreset |
| Apache с модулем PHP (XAMPP, Apache Lounge) | Служба Apache, именно restart | Restart-Service -Name "имя_службы" (имя смотрите через Get-Service) |
| Задания планировщика с php.exe | Ничего, следующий запуск использует новый файл | Проверить путь в задании |
Теперь настройки FastCGI. В разделе fastCgi у приложения PHP есть атрибут instanceMaxRequests, который задаёт, сколько запросов обрабатывает один процесс до пересоздания, и переменная окружения PHP_FCGI_MAX_REQUESTS. По документации Microsoft, значение переменной должно быть не больше instanceMaxRequests. Типовой пример Microsoft использует 10000 для обоих значений. Мы держим эти два значения согласованными и не пытаемся лечить ими уязвимость: пересоздание процессов по счётчику сокращает время жизни воркера, но не закрывает ошибку, и в источниках оно как защита не названо. Это гигиена, а не заплатка.
%windir%\system32\inetsrv\appcmd.exe set config -section:system.webServer/fastCgi /+"[fullPath='C:\php\php-cgi.exe'].environmentVariables.[name='PHP_FCGI_MAX_REQUESTS',value='10000']" /commit:apphostКоманда из документации Microsoft добавляет переменную окружения к уже существующему FastCGI-приложению с таким fullPath; если переменная есть, appcmd сообщит о дубле. Значение instanceMaxRequests проще посмотреть и поменять в диспетчере IIS: FastCGI Settings, выбрать приложение, Edit. Ещё один полезный атрибут — monitorChangesTo с путём к php.ini: при изменении файла IIS перезапустит процессы PHP сам. Мы всё равно перезапускаем пул вручную после каждой правки, чтобы не гадать, подхвачено изменение или нет.
Проверка состоит из трёх частей. Первая: запущенные процессы должны быть новыми и из ожидаемого каталога. Вторая: версия внутри пула должна совпадать с исправленной. Третья: сайт должен нормально работать, включая загрузку файлов и оформление заказа. Версию изнутри пула лучше проверять не по файлам, а запросом к самому сайту, например через временный скрипт, который сразу удаляется. Скрипт с phpinfo() не оставляйте: он раскрывает конфигурацию любому, кто его найдёт.
Get-CimInstance Win32_Process -Filter "Name='php-cgi.exe'" |
Select-Object ProcessId, ExecutablePath, CreationDate # CreationDate должна быть после замены
& C:\php\php.exe -r "echo PHP_VERSION, PHP_EOL;"
& C:\php\php.exe -i | Select-String 'Loaded Configuration File|upload_progress.enabled|memory_limit'
Get-ChildItem C:\inetpub\logs\LogFiles -Recurse -Filter *.log | Sort-Object LastWriteTime -Descending | Select-Object -First 3Если после обновления сайт отвечает ошибкой 500, открывайте журнал приложений Windows и логи IIS. Три причины встречаются чаще остальных: не установлен Visual C++ Redistributable нужной версии, в php.ini осталась директива расширения от другой ветки и не загрузилась, или в обработчике и в fastCgi оказались разные пути. Откат простой: остановить пул, вернуть переименованный каталог, запустить пул. Поэтому старую папку и держим несколько дней.
Если обновить сегодня нельзя: временные меры в IIS
Временные меры нужны, когда исправленной сборки под вашу ветку ещё нет, когда приложение не проверено на новой версии или когда окно обслуживания только завтра. Повторим главное: они частичные и обновление не заменяют. Самое действенное из того, что можно сделать на IIS, — не пускать multipart/form-data туда, где загрузки файлов не нужны. Все известные триггеры используют именно такие запросы, потому что PHP разбирает их тело до кода приложения. Источники называют этот приём в варианте для nginx и Apache; ниже его эквивалент для IIS.
Первый вариант — правило URL Rewrite. Модуль не входит в состав IIS по умолчанию и ставится отдельным msi-пакетом с официальной страницы IIS. Правило смотрит на заголовок Content-Type через серверную переменную HTTP_CONTENT_TYPE (по документации заголовки запроса доступны как переменные с префиксом HTTP_, дефисы заменяются на подчёркивания) и отвечает 403, если это multipart и путь не входит в список исключений. Список исключений нужен для страниц, где загрузка файлов — часть работы. Пути /upload и /order-files в примере условные, подставьте свои. Заодно файл задаёт лимит размера тела запроса 8388608 байт (8 МБ): Patchstack советует post_max_size 8M для сайтов без загрузок и 32M для сайтов с загрузками, для ограничения на уровне IIS используется атрибут maxAllowedContentLength и при превышении возвращается 413.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Block multipart outside upload paths" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{HTTP_CONTENT_TYPE}" pattern="multipart/form-data" />
<add input="{REQUEST_URI}" pattern="^/(upload|order-files)(/|\?|$)" negate="true" />
</conditions>
<action type="CustomResponse" statusCode="403" statusReason="Forbidden" statusDescription="Forbidden" />
</rule>
</rules>
</rewrite>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="8388608" />
</requestFiltering>
</security>
</system.webServer>
</configuration>Второй вариант — штатный Request Filtering, ставить ничего не нужно. Правило filteringRule сканирует заголовок Content-Type у запросов к файлам .php и отклоняет те, где есть строка multipart/form-data. Нюансы: такой запрос IIS отклоняет с кодом 404 и подкодом 404.19 («Denied by filtering rule»), а не с 403, и исключений по путям у этого правила нет. Чтобы разрешить загрузки в отдельной папке, положите в неё свой web.config, который снимает правило через remove.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<filteringRules>
<filteringRule name="Block multipart PHP" scanUrl="false" scanQueryString="false">
<scanHeaders>
<add requestHeader="Content-Type" />
</scanHeaders>
<appliesTo>
<add fileExtension=".php" />
</appliesTo>
<denyStrings>
<add string="multipart/form-data" />
</denyStrings>
</filteringRule>
</filteringRules>
</requestFiltering>
</security>
</system.webServer>
</configuration><?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<filteringRules>
<remove name="Block multipart PHP" />
</filteringRules>
</requestFiltering>
</security>
</system.webServer>
</configuration>Проверьте синтаксис до применения. Мы сверили элементы и атрибуты с документацией Microsoft для URL Rewrite (rules, conditions, action CustomResponse) и Request Filtering (filteringRule, scanHeaders, appliesTo, denyStrings, requestLimits). Первичная проверка: файл должен открываться как XML, а IIS не должен отвечать ошибкой 500.19 на любой запрос. Ошибка 500.19 означает, что IIS не принял конфигурацию. После правки откройте сайт и выполните тестовые запросы.
[xml](Get-Content C:\inetpub\wwwroot\web.config) | Out-Null # путь к web.config сайта, ошибки разбора будут показаны
curl.exe -s -o NUL -w "%{http_code}\n" https://site.example/index.php # обычный GET, ожидаем 200
curl.exe -s -o NUL -w "%{http_code}\n" -F "a=b" https://site.example/index.php # multipart, ожидаем 403 (URL Rewrite) или 404 (Request Filtering)
curl.exe -s -o NUL -w "%{http_code}\n" -F "a=b" https://site.example/upload/ # разрешённый путь, ожидаем ответ приложенияЧто ломает такая мера: загрузки файлов, часть форм (корзина и оформление заказа), личные кабинеты с вложениями. Источники прямо требуют протестировать оформление заказа. Блокировка на путях исключений не защищает: там уязвимость остаётся открытой. Поэтому у исключений нужны дополнительные ограничения: доступ только после входа, по возможности только с адресов компании (в IIS для этого есть ipSecurity) и малые лимиты размера.
Второй слой — настройки PHP. Отключаем session.upload_progress.enabled в php.ini и перезапускаем все процессы. Через ini_set() и .user.ini это сделать нельзя, нужен php.ini или аргумент командной строки -d в настройках FastCGI. Для PHP 8.5 добавьте fatal_error_backtraces=0. Мера не закрывает проблему целиком: Джина Питер Банъярд подчёркивает, что одного отключения мало. Что до memory_limit, Sansec советует значение -1, Patchstack для CVE-2026-109618 предлагает задать его так, чтобы приложение не смогло переопределить. У PHP-FPM для этого есть php_admin_value, у IIS аналога мы не знаем, поэтому проверьте, что код приложения не вызывает ini_set('memory_limit', ...). Цена значения -1: один запрос может съесть всю память сервера, поэтому следите за свободной памятью. Не используйте RLIMIT_AS и RLIMIT_DATA, Patchstack против них.
; php.ini, перезапустить пул после правки
session.upload_progress.enabled = 0
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 2M
; только для PHP 8.5
fatal_error_backtraces = 0Значения post_max_size и upload_max_filesize подбирайте по реальной работе сайта: Patchstack называет 8M для сайтов без загрузок и 32M для сайтов с загрузками как ориентир. Если нужно задать отдельные параметры для одного сайта, их можно передать в аргументах FastCGI-приложения (-d session.upload_progress.enabled=0) и продублировать после вертикальной черты в scriptProcessor обработчика, как показано в документации Microsoft для сайтовых обработчиков. Делайте это только если понимаете, как два значения связаны.
Полная остановка тоже вариант. Если сайт не критичен прямо сегодня, остановите пул или отдайте статическую страницу, либо пропускайте только запросы GET и HEAD. NCSC рекомендует именно такой порядок: обновить, при невозможности применить меры, а если и это долго, рассмотреть отключение системы. Отдельно: Cloudflare выкатил аварийные правила WAF, но они работают, только если сервер принимает трафик исключительно от Cloudflare, и тоже названы временными.
Мини-кейс и ошибки, которые мы видим на Windows-серверах
Возьмём вымышленную типографию «Гутенберг-М» с небольшим офисом. Сайт на IIS принимает заказы, клиенты загружают макеты, и загрузка файлов здесь основная функция, а не побочная. Одновременно на том же сервере живёт старый XAMPP, где когда-то подняли внутренний сервис расчёта тиража. Блокировать multipart на всём сайте нельзя: остановятся заказы. Поэтому порядок такой. Сначала инвентаризация: нашлись две копии PHP под IIS и ещё одна внутри XAMPP. Затем XAMPP отключаем от сети, так как внутреннему сервису внешний доступ не нужен. Для основного сайта временная мера действует на всём, кроме страницы загрузки макетов, а у страницы загрузки ставим малые лимиты и наблюдение по логам. Обновление сборки назначаем на ближайшее окно обслуживания, как только нужный номер версии подтверждён на странице загрузки.
Первая типичная ошибка: обновили только ту копию PHP, которую видно в IIS Manager. На сервере остаются php.exe в XAMPP, OpenServer, каталогах со старыми тестами и пакетах разработчиков. Про любую копию, которая слушает порт или запускается службой, нужно знать. Сопоставляйте список файлов со списком процессов и слушающих портов: Get-NetTCPConnection -State Listen | Select LocalPort, OwningProcess.
Вторая ошибка: путают Thread Safe и Non Thread Safe. После замены в обработчик подставили не тот вариант, расширения не загрузились, сайт отвечает 500. Третья: забыли Visual C++ Redistributable, поэтому php-cgi.exe не стартует. Четвёртая: перенесли старый php.ini целиком вместо сверки с php.ini-production, и вместе с ним переехали устаревшие директивы. Пятая: перезапустили пул, но не Apache, который тоже обслуживал часть сайтов, и один из серверов остался на старом коде.
Шестая ошибка касается чужих продуктов с собственным PHP. Веб-публикация 1С, остатки веб-окружения 1С-Битрикс для Windows, веб-интерфейсы Zabbix и GLPI, phpMyAdmin и подобные системы нередко везут свой интерпретатор в своём каталоге. Заменять его самостоятельно в таких случаях рискованно: продукт может быть привязан к конкретной версии. Что делать с Битриксом, мы разобрали в статье про 1С-Битрикс и PHP, а про веб-интерфейсы мониторинга и учёта — в материале про Zabbix, GLPI и FreePBX. Если по продукту нет заявления вендора, на 21 октября не подтверждено, проверьте у вендора.
Седьмая: после обновления считают работу законченной. Прежде чем закрывать задачу, посмотрите логи на признаки эксплуатации: необычные multipart-запросы с последующими ошибками memory_limit, повторяющиеся HTTP 500, аномально большие или битые куки PHPSESSID, необъяснимые перезапуски процессов PHP. Как это искать в журналах, описано в статье про признаки взлома в логах. Это не доказательство ни в ту, ни в другую сторону, но повод насторожиться. При подозрении на компрометацию мы обновляем PHP, считаем сервер возможно скомпрометированным и передаём владельцу список ключей и паролей, которые хранились на сервере, на ротацию. Пароли живых учёток сами не меняем: решение принимает владелец.
Если нужна помощь с инвентаризацией, обновлением и проверкой, мы делаем это в рамках услуг обслуживания серверов и открытого ПО. Про обновление на Linux-серверах рядом с вашим Windows-сервером читайте в статье про PHP2Shell на Debian и Ubuntu, если у вас смешанная среда.
Частые вопросы
Затронут ли PHP на Windows и IIS уязвимостями CVE-2026-109618 и CVE-2026-109619?
Да. Уязвимости находятся в ядре интерпретатора, поэтому затронуты php-cgi.exe под IIS FastCGI и модуль для Apache. NCSC пишет, что затронуты любые версии PHP. Конфигурация по умолчанию уязвима, а запрос идёт без аутентификации из сети. Проверьте версию через php.exe -v и сравните с 8.5.12, 8.4.27, 8.3.36, 8.2.35.
Есть ли исправленные сборки PHP на windows.php.net?
Проверьте сами на странице загрузки. Upstream выпустил 8.5.12, 8.4.27, 8.3.36 и 8.2.35, но подтверждения, что именно эти Windows-сборки уже лежат на windows.php.net, мы на 21 октября 2026 не получили. Смотрите номер версии в имени архива. Если нужного номера нет, применяйте временные меры и повторяйте проверку ежедневно.
Можно ли обновить PHP на Windows через Web Platform Installer?
Нет, рассчитывать на него не стоит. Microsoft сообщила, что поддержка Web Platform Installer закончилась 1 июля 2022 года, а на странице загрузки он значится выведенным с 31 декабря 2022 года. Используйте ручную установку: скачать архив с windows.php.net, проверить, распаковать, перенести php.ini, перезапустить пул и проверить версию.
Достаточно ли заблокировать multipart/form-data в IIS?
Нет, это только частичная мера. Блокировка закрывает известные триггеры на путях без загрузок, но на путях-исключениях уязвимость остаётся, и она ломает загрузки и часть форм. Единственное надёжное исправление — обновление PHP до 8.5.12, 8.4.27, 8.3.36 или 8.2.35. Блокировку используйте только до обновления и проверьте оформление заказа.
Помогает ли перезапуск пула и PHP_FCGI_MAX_REQUESTS?
Перезапуск пула нужен после любой замены файлов, иначе работают старые процессы. А PHP_FCGI_MAX_REQUESTS и instanceMaxRequests лишь сокращают время жизни процесса и не закрывают уязвимость, источники их как защиту не называют. Держите значения согласованными: переменная не больше instanceMaxRequests. Реальная защита — обновлённая сборка и проверка версии после перезапуска.
Источники
- Sansec: PHP2Shell — Разбор уязвимостей, триггеры, временные меры (memory_limit, блок multipart): https://sansec.io/research/php2shell
- Patchstack: предупреждение о критической уязвимости PHP — Исправленные версии, лимиты post_max_size, признаки эксплуатации, бэкпорты из исходников: https://patchstack.com/articles/php-major-vulnerability-alert-10-2026/
- NCSC UK: Critical security issue affecting PHP — Рекомендации по инвентаризации всех PHP и порядку действий: https://www.ncsc.gov.uk/news/critical-security-issue-affecting-php
- Microsoft Learn: FastCGI application — Атрибуты instanceMaxRequests, monitorChangesTo, PHP_FCGI_MAX_REQUESTS, appcmd: https://learn.microsoft.com/en-us/iis/configuration/system.webserver/fastcgi/application/
- Microsoft Learn: Request Filtering filteringRule — Синтаксис filteringRule, scanHeaders, denyStrings, подкод 404.19: https://learn.microsoft.com/en-us/iis/configuration/system.webserver/security/requestfiltering/filteringrules/filteringrule
- Microsoft Learn: URL Rewrite Module Configuration Reference — Условия по HTTP-заголовкам (HTTP_*), действие CustomResponse: https://learn.microsoft.com/en-us/iis/extensions/url-rewrite-module/url-rewrite-module-configuration-reference
- IIS Team Blog: Web Platform Installer end of support — Даты окончания поддержки Web Platform Installer: https://blogs.iis.net/iisteam/web-platform-installer-end-of-support-feed


