Разбор аварии

Сайт упал после обновления WordPress 7.1: виноват WP Rocket

Обновлено 20 августа 2026

За одно утро мы подняли шесть сайтов с одной и той же аварией. Симптом одинаковый: обновили WordPress — и сайт лёг целиком. Не «поехала вёрстка», а именно белый экран или 500 на всём сразу: фронт, админка, корзина, формы, крон.

Виновник — WP Rocket. Точнее, его модуль интеграции с Cloudflare, который на PHP 8 спотыкается о новый способ хранения хуков в WordPress.

Ниже: как опознать за минуту, как починить за две, и почему это не разовая случайность, а мина, которая ждёт каждый сайт с этим плагином.

Симптом

Как выглядит ошибка

Если у вас включён вывод ошибок или вы заглянули в лог, вы увидите ровно это:

php fatal errorлог сервера
PHP Fatal error: Uncaught TypeError: substr(): Argument #1 ($string)
must be of type string, int given
in .../wp-content/plugins/wp-rocket/inc/ThirdParty/Plugins/CDN/Cloudflare.php:496

Дальше идёт трейс:

stack trace
#0 .../CDN/Cloudflare.php(496): substr(6556, -24)
#1 .../CDN/Cloudflare.php(469): unregister_callback('deleted_post', 'purgeCacheByRel...')
#2 .../wp-includes/class-wp-hook.php(353): unregister_cloudflare_clean_on_post('')
...
#5 .../wp-settings.php(779): do_action('init')

Номер строки у вас может быть другим — 496 или 562, зависит от версии плагина. Число в substr(6556, -24) каждый раз новое: 6243, 7175, 8840. Это нормально, дальше объясню почему.

Если вывод ошибок выключен, вы увидите просто белый экран или «На сайте возникла критическая ошибка».

Это ваш случай?

Три признака, все должны совпасть:

  1. Недавно обновляли WordPress (у нас падало на 7.1).
  2. Установлен WP Rocket — любой версии.
  3. Легло всё сразу, включая /wp-admin/.

Плагин Cloudflare при этом может быть не установлен вовсе. Из шести наших сайтов он не стоял ни на одном. Это ключевой момент, к нему вернёмся.

Быстрая проверка через SSH

bash
grep -n 'substr( $key, - strlen( $method ) )' \
  wp-content/plugins/wp-rocket/inc/ThirdParty/Plugins/CDN/Cloudflare.php

Строка нашлась — сайт под ударом. Ничего не выдало — либо WP Rocket другой версии, либо патч уже наложен.


Решение

Вариант 1: плагин, если админка ещё открывается

Мы собрали плагин, который находит проблемную строку и правит её сам. Ставится обычным способом, без SSH и FTP.

WP Rocket PHP 8 Guard

версия 1.2 · 6 КБ · GPLv2

Скачать плагин

Установка: Плагины → Добавить новый → Загрузить плагин → выбрать ZIP → Установить → Активировать.

Патч накладывается сразу при активации. На странице «Плагины» появится строка состояния — зелёная «патч на месте» означает, что всё сделано.

Зачем он нужен постоянно, а не разово. Правка живёт в файле чужого плагина. При каждом обновлении WP Rocket файл перезаписывается и патч слетает — сайт ляжет снова, молча и в неудобный момент. Плагин проверяет файл после каждого обновления и накладывает патч заново.

Он безвреден там, где WP Rocket нет: первая же проверка — существует ли файл. Если нет, плагин просто ждёт.

Вариант 2: правка файла, если сайт уже лежит

Когда админка недоступна, залить плагин через неё не выйдет. Правим напрямую.

Файл:

путь
wp-content/plugins/wp-rocket/inc/ThirdParty/Plugins/CDN/Cloudflare.php

Находим строку (496 или 562):

php · было
if ( substr( $key, - strlen( $method ) ) !== $method ) {

Меняем на:

php · стало
if ( substr( (string) $key, - strlen( $method ) ) !== $method ) {

Всё изменение — приведение к строке. Девять символов.

Через SSH, с проверкой

Если правите руками, есть риск задеть не ту строку. Команда ниже сначала убеждается, что совпадение ровно одно, и только потом пишет:

bashкопировать целиком
F=wp-content/plugins/wp-rocket/inc/ThirdParty/Plugins/CDN/Cloudflare.php
perl -0777 -e '
my $f=$ARGV[0];
open(my $h,"<",$f) or die "OPEN_FAIL\n"; local $/; my $s=<$h>; close $h;
if ($s =~ /substr\(\s*\(string\)\s*\$key/) { print "ALREADY_PATCHED\n"; exit 0 }
my $re = qr/substr\(\s*\$key\s*,\s*-\s*strlen\(\s*\$method\s*\)\s*\)/;
my $n = () = ($s =~ /$re/g);
print "ANCHOR_COUNT=$n\n";
if ($n != 1) { print "ABORT\n"; exit 1 }
$s =~ s/$re/substr( (string) \$key, - strlen( \$method ) )/;
open(my $o,">",$f.".tmp") or die; print $o $s; close $o;
rename($f.".tmp",$f) or die;
print "PATCHED\n";
' "$F"

Ждём ANCHOR_COUNT=1 и PATCHED. Запись идёт через временный файл и rename, поэтому посетители не увидят полузаписанный файл.

После этого сайт поднимается сразу — перезапускать ничего не нужно. Дальше стоит поставить плагин из первого варианта, чтобы правка не слетела при следующем обновлении.


Разбор

Почему это происходит

Разберём механику — она объясняет, почему ошибка выглядит случайной и почему её не лечит удаление Cloudflare.

Ключи хуков стали числами

WordPress держит все колбэки в глобальном массиве $wp_filter. Ключ для каждого строит функция _wp_filter_build_unique_id() в wp-includes/plugin.php:

php · wp-includes/plugin.php
if ( is_object( $callback ) ) {
        return (string) spl_object_id( $callback );
}

Для колбэка-замыкания ключ получается чистым числом в виде строки: "6556".

А дальше вступает многолетнее правило PHP: при записи в массив числовая строка автоматически превращается в int. Строка "6556" становится числом 6556.

Именно поэтому число в трейсе каждый раз разное — spl_object_id выдаёт новый идентификатор при каждом запросе. Ошибка выглядит плавающей, хотя на деле совершенно детерминирована.

WP Rocket этого не ждал

Файл Cloudflare.php объявлен со строгой типизацией — declare(strict_types=1). Метод перебирает колбэки и передаёт ключ в substr():

php · wp-rocket
foreach ( $original_wp_filter[ $priority ] as $key => $config ) {
        if ( substr( $key, - strlen( $method ) ) !== $method ) {

На PHP 7 число молча привелось бы к строке. На PHP 8 со строгой типизацией это фатальная ошибка.

Почему падает даже без Cloudflare

Самое неочевидное. В классе десяток методов, и почти все начинаются с проверки:

php · wp-rocket
if ( ! $this->is_plugin_active() ) {
        return;
}

Метод unregister_cloudflare_clean_on_post()единственный без этой проверки. При этом он подписан на хук init, то есть выполняется на каждом запросе к сайту.

Отсюда и картина: плагин Cloudflare не установлен, учётные данные не заведены, CDN не используется — а сайт всё равно ложится.

Почему ложится всё сразу

init — один из самых ранних хуков WordPress, срабатывает в wp-settings.php до того, как система решит, куда направить запрос. Поэтому умирает не какая-то страница, а любой вход в WordPress: фронт, /wp-admin/, admin-ajax.php (то есть все формы и корзина), wp-cron.php.


Внимание

Сайт может выглядеть живым

Отдельно стоит предупредить. На одном из наших сайтов главная отдавала честные 200, пока админка уже возвращала 500.

Причина в том, что закешированные страницы WP Rocket отдаёт мимо PHP — прямо из статики. Пока кеш не протух, посетители видят рабочий сайт.

Что при этом происходит на самом деле:

  • новые страницы не кешируются — их некому создать;
  • любая публикация сбрасывает кеш, и после этого ложится и фронт;
  • формы и корзина мертвы (это admin-ajax.php);
  • wp-cron не работает: письма не уходят, задачи не выполняются, товары не выгружаются.

Так что «главная открывается» — не повод расслабиться. Проверяйте админку.

Какие версии затронуты

Мы наблюдали падение на этих версиях WP Rocket:

Версия WP RocketСтрока
3.18.2496
3.20.1.2496
3.21.3562
3.23.1.1562
3.23.2.1562

Диапазон широкий, свежие версии в списке есть. То есть обновление WP Rocket проблему не решает — по крайней мере на момент публикации. Ждать апдейта, сидя с упавшим сайтом, смысла нет.

Подтверждённая связка — WordPress 7.1. На сайтах с 7.0.4 уязвимый код в файле присутствует, но падений мы не видели: похоже, там ещё не сложились условия. Считать их безопасными не стоит — при обновлении ядра ляжет и они.

Если вы ещё не обновились

Самый дешёвый сценарий: поставить плагин до обновления WordPress, а не после.

  1. Ставим и активируем плагин на всех сайтах с WP Rocket.
  2. Убеждаемся, что на странице «Плагины» показано «патч на месте».
  3. Обновляем ядро.

Обновление пройдёт незаметно вместо аварийного вечера. Если у вас несколько сайтов, разложить плагин заранее гораздо дешевле, чем поднимать их по одному по мере падения — тем более что часть из них будет выглядеть живой за счёт кеша.

Когда плагин можно удалить

Когда WP Rocket выпустит версию с исправлением. Плагин заметит это сам: если знакомой строки в коде больше нет, а патча тоже нет, он покажет сообщение, что вендор всё починил и сторожа можно отключить.

При деактивации патч не откатывается — иначе сайт немедленно ляжет.


Коротко

Симптом
После обновления WordPress лёг весь сайт, включая админку.
Причина
WP Rocket передаёт числовой ключ хука в substr() при строгой типизации.
Лечение
Приведение к строке — substr( (string) $key, ... ).
Нюанс
Правка живёт в чужом плагине и слетит при его обновлении.
Решение
Плагин-сторож, который держит патч на месте.

WP Rocket PHP 8 Guard

версия 1.2 · 6 КБ · GPLv2

Скачать плагин
Вопросы

Частые вопросы

Можно просто отключить WP Rocket?

Да, сайт поднимется. Но вы потеряете кеширование, и скорость сайта просядет — иногда в разы. Патч решает вопрос без этой платы.

А если удалить плагин Cloudflare?

Не поможет. Проблемный метод — единственный в классе без проверки на активность Cloudflare. Он выполняется независимо от того, установлен плагин или нет.

Не сломает ли патч логику WP Rocket?

Нет. Настоящие колбэки WP Rocket имеют ключ вида <идентификатор><имя_метода> — они по-прежнему находятся и обрабатываются как задумано. Изменение касается только числовых ключей чужих замыканий, которые и раньше не должны были совпадать.

Почему число в ошибке каждый раз разное?

Это идентификатор объекта в памяти, spl_object_id. Он выдаётся заново при каждом запросе. Ошибка выглядит случайной, но воспроизводится стопроцентно.

У меня PHP 7, я в безопасности?

На PHP 7 число молча приводится к строке, фатала не будет. Но PHP 7 давно снят с поддержки, и оставаться на нём ради обхода одного бага — плохая сделка.


Столкнулись с этим на нескольких сайтах и не хотите разбираться вручную — напишите нам, поможем разложить решение по всему парку.

yandex
socseti
jad
jad
jad
sayt2
avito2