Перейти к содержимому
Noroxi
Исследования9 мин

Page Optimize: неаутентифицированный DoS через переполнение кэша (CWE-770)

Ключ кэша Page Optimize строится из сырого URI запроса, и анонимный посетитель может завалить сайт дублями кэш-файлов, исчерпав диск и inode (CWE-770).

Команда NoroxiТестирование на проникновение и консалтинг

Аннотация. Page Optimize — плагин производительности WordPress от Automattic с более чем 200 000 активных установок. Ключ кэша он строит из сырого URI запроса, а не из разрешённого и проверенного списка файлов. Эту рассогласованность неаутентифицированный удалённый посетитель может превратить в принудительную запись неограниченного числа дублей кэш-файлов и исчерпать диск и inode (CWE-770). Ниже — уязвимый код, концептуальный путь к уязвимому месту, влияние на доступность, устранение и рекомендации по обнаружению для владельцев сайтов. В рамках скоординированного раскрытия производитель уведомлён, а рабочий proof of concept остаётся закрытым до выхода патча.

Ключевые слова: Page Optimize, WordPress, CWE-770, неконтролируемое потребление ресурсов, отказ в обслуживании, ключ кэша, исчерпание диска, исчерпание inode.

1. Введение

Page Optimize объединяет JavaScript и CSS сайта и отдаёт их через отдельную конечную точку /_static/. Установок больше 200 000, поэтому обработка запросов в этом плагине — заметная часть поверхности атаки WordPress.

Код кэширования выполняется рано, часто и для каждого посетителя — именно поэтому его стоит разбирать внимательно. Слой, который создаёт файлы по запросу, обязан аккуратно выбрать, что именно идентифицирует запись кэша. Выбери эту идентичность небрежно — и функция, призванная экономить работу, начинает порождать работу без предела. В Page Optimize идентичность выбрана неверно, и анонимный посетитель способен довести кэш до неконтролируемого потребления ресурсов (CWE-770). Текст написан так, чтобы разработчики узнали этот шаблон, а владельцы сайтов — обнаружили и сдержали проблему.

2. Затронутый компонент

ПлагинPage Optimize (Automattic) — slug page-optimize
Тестируемая версия0.6.3 (актуальная/последняя на момент тестирования)
Активные установки200 000+
Тип уязвимостиНеконтролируемое потребление ресурсов / отказ в обслуживании
CWECWE-770 — выделение ресурсов без ограничений (также CWE-400)
Требуемые привилегииНет — неаутентифицированно, удалённо
Взаимодействие пользователяНет
CVSS 3.15.3 (Средняя) — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L (растёт к 7.5 / A:H при устойчивом флуде, полностью заполняющем диск)
СтатусСвежая (0-day), подтверждена в коде и воспроизведена в localhost-лаборатории на актуальной версии; без патча

3. Технический анализ

3.1 Конечная точка отвечает всем

Плагин регистрирует диспетчер в глобальной области. На каждом фронтенд-запросе — считая и запросы незалогиненных посетителей — он читает URI запроса и передаёт управление статическому сервису, как только путь начинается с /_static/. Проверки nonce, прав или аутентификации на этом пути нет:

php
// page-optimize.php (lines 28-31)if ( isset( $_SERVER['REQUEST_URI'] ) && '/_static/' === substr( $_SERVER['REQUEST_URI'], 0, 9 ) ) {    require_once __DIR__ . '/service.php';    exit;}

Отдавать объединённые ресурсы анонимным посетителям — это задумано. Проблема начинается при промахе кэша (cache miss).

3.2 Ключ кэша — это сырой URI запроса

При промахе сервис собирает объединённый вывод и записывает два файла: файл полезной нагрузки и файл метаданных. Оба именуются по md5 от сырого URI запроса — вместе со всей строкой запроса:

php
// service.php (lines 27-30, 73-76)$request_uri_hash = md5( $_SERVER['REQUEST_URI'] ); // <-- ключ — это необработанный URI, управляемый атакующим$cache_file       = PAGE_OPTIMIZE_CACHE_DIR . "/page-optimize-cache-$request_uri_hash";$cache_file_meta  = PAGE_OPTIMIZE_CACHE_DIR . "/page-optimize-cache-meta-$request_uri_hash";...file_put_contents( $cache_file, $content );file_put_contents( $cache_file_meta, json_encode( array( 'headers' => $headers ) ) );

3.3 Ключ несёт байты, от которых вывод не зависит

В этом вся суть. Прежде чем решить, какие файлы объединять, сервис отбрасывает всё начиная с завершающего маркера ?version. Значит, суффикс версии или мусора не влияет ни на разрешённый список файлов, ни на содержимое вывода — и всё же остаётся частью md5-ключа из §3.2:

php
// service.php (lines 117-121) — суффикс версии отбрасывается ДО разрешения файлов$version_string_pos = strpos( $args, '?' );if ( false !== $version_string_pos ) {    $args = substr( $args, 0, $version_string_pos );}

Почему это ошибка. Ключ и кэшируемое значение разошлись. Значение — чистая функция от разрешённого и проверенного списка файлов; ключ — функция от сырого URI, набитого байтами, которыми управляет атакующий и которые вывод отбрасывает. Два запроса с побайтово одинаковым выводом всё равно сохраняются под разными ключами. Правильный кэш ключуется ровно по тем входным данным, что определяют значение, — ни больше, ни меньше.

Дефект держится, а не проходит, из-за двух деталей:

  • Кэширование включено по умолчанию: PAGE_OPTIMIZE_CACHE_DIR определён всегда (wp-content/cache/page_optimize).
  • Очистка (page_optimize_cache_cleanup) идёт только по ежедневному WP-cron и удаляет лишь файлы старше 24 часов; сам WP-cron срабатывает только при органическом трафике. Поэтому свежие файлы живут не меньше суток.

4. Как достигается уязвимое место (концептуально)

Привилегии не нужны: диспетчер /_static/ по замыслу отвечает на анонимные GET-запросы. Атакующему достаточно одного общедоступного факта — действительного пути к ресурсу, который сайт реально отдаёт. Распространённые ресурсы фреймворков, поставляемые с WordPress, есть практически везде.

Дальше всё следует из §3.3. Раз ключ включает байты, от которых вывод не зависит, одно и то же базовое объединение адресуется практически неограниченным набором ключей: каждая вариация игнорируемого суффикса — новый md5 и новая пара файлов на диске с идентичным содержимым. Нет ограничения по IP, нет потолка на число записей, нет верхней границы на общий размер кэша, нет ограничения частоты на конечной точке. Каждый запрос, который сервер считает промахом, выполняет новую запись.

Итог — отказ в обслуживании по объёму и накоплению: сервер можно вынудить записать куда больше данных и куда больше отдельных файлов, чем когда-либо оправдал бы единственный реальный ресурс. Каждая запись расходует две записи каталога (два inode) и живёт не меньше суток, так что под угрозой и бюджет свободного места, и таблица inode. Последовательность запросов, значения суффиксов и любые команды повтора мы намеренно не публикуем; описанного механизма хватает, чтобы понять проблему и защититься, а рабочие детали ждут исправления.

5. Оценка воздействия

Неаутентифицированный удалённый субъект способен устойчиво и без предела наращивать каталог кэша плагина. Путей исчерпания два:

  1. Исчерпание свободного места — записанные байты растут примерно пропорционально размеру отдаваемого ресурса; усиление близко к 1:1, поэтому заметный сбой требует устойчивого объёма запросов.
  2. Исчерпание inode — каждая запись — это пара небольших файлов, так что число inode растёт быстро и может опустошить таблицу inode задолго до того, как сырые байты заполнят диск; особенно это заметно на небольших VPS.

Стоит исчерпать любой из бюджетов — и все операции WordPress, которым нужна запись (временные файлы БД, загрузка медиа, данные сессий, дальнейшее кэширование), начинают падать, а сайт перестаёт работать. Поэтому базовая оценка держится на 5.3 (Средняя) за частичное воздействие на доступность (A:L) и поднимается к 7.5 / A:H при устойчивом флуде, целиком заполняющем диск или таблицу inode. Воздействия на конфиденциальность и целостность нет; дело исключительно в доступности.

Ради честности очертим и то, чем находка не является. Разрешение файлов усилено: .. и обход с null-байтом блокируются, несуществующие пути возвращают 404 без записи — значит, это не обход каталога и не ошибка произвольного места записи. Необязательная ветка gzuncompress() не даёт практичной «декомпрессионной бомбы». Настройки администратора используют базовый Settings API за nonce и manage_options. Остаётся подлинный дефект исчерпания ресурсов, честно отнесённый к Средним, а не усилитель с высоким коэффициентом в маскировке.

6. Устранение

Решающее исправление — выровнять ключ кэша с тем значением, которое он хранит:

php
// Ключуйте кэш по РАЗРЕШЁННОМУ, ПРОВЕРЕННОМУ списку файлов, а не по необработанному URI.// После отбрасывания суффикса версии и разрешения/проверки путей:$resolved = page_optimize_resolve_and_validate( $args ); // упорядоченный, проверенный список файлов$request_uri_hash = md5( implode( "\0", $resolved ) );    // ключ == ровно те входные данные,                                                           // что определяют вывод

Ключевание по разрешённому и проверенному списку файлов сводит все вариации с мусорным суффиксом одного запроса к единственной записи и устраняет размножение в самом истоке. В качестве эшелонированной защиты плагину также стоит:

  • Ограничить число записей кэша и/или общий размер каталога кэша, вытесняя по границе, а не только по 24-часовому возрасту.
  • Ограничивать частоту обращений к конечной точке /_static/ на клиента.
  • Запускать очистку при давлении записи, а не только по ежедневному cron, зависящему от органического трафика.

Пока официального исправления нет, владельцам сайтов следует применить меры из §7.

7. Обнаружение и усиление для владельцев сайтов

Всё ниже выведено из описанной механики; никаких выдуманных функций продукта.

  • Следите за каталогом кэша. Отслеживайте wp-content/cache/page_optimize на аномальный рост числа файлов, а не только размера. Самый чёткий признак — быстро растущее число пар page-optimize-cache-* / page-optimize-cache-meta-* с идентичным содержимым полезной нагрузки.
  • Мониторьте inode, а не только диск. На небольших хостах таблица inode исчерпывается первой. Настройте оповещения и по использованию inode, и по свободному месту (df -i в Linux).
  • Просматривайте журналы доступа на шаблоны /_static/. Ищите множество запросов к одному и тому же базовому ресурсу, различающихся лишь меняющимся завершающим суффиксом, особенно из одного источника или узкого их набора.
  • Минимизируйте экспозицию. Если объединение не нужно, отключите или удалите плагин. Если нужно, рассмотрите ограничение или отключение дискового кэширования там, где это позволяет ваш стек.
  • Виртуальный патч / WAF. WAF или граничное правило может ограничить частоту /_static/ и отклонять запросы, чья строка запроса несёт суффикс высокой кардинальности, не влияющий на вывод. Считайте это временной мерой, а не заменой исправления ключа кэша.
  • Запланированная очистка. Встроенная очистка зависит от срабатывания WP-cron, поэтому подкрепите её настоящим системным cron, чтобы устаревшие кэш-файлы действительно удалялись.

8. Более широкий урок

Принцип прост и применим широко: ключ кэша должен быть функцией ровно тех входных данных, что определяют кэшируемое значение, — ни больше, ни меньше. Попадут в ключ управляемые атакующим байты, которые значение игнорирует, — и кэш можно заставить хранить неограниченные дубли одного содержимого; выпадут из ключа байты, от которых значение зависит, — возникнет путаница или отравление кэша. Любое расхождение — дефект. Та же дисциплина распространяется на мемоизацию, таблицы дедупликации, корзины ограничителей частоты и любой ресурс «по требованию», ключуемый данными запроса. Сочетайте верное ключевание с явными потолками ресурсов — число записей, общий размер, ограничения частоты на клиента, — потому что кэш без границы это аллокатор, которым управляет анонимный посетитель.

9. Ответственное раскрытие

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

Всё тестирование шло исключительно в изолированной, привязанной к localhost Docker-лаборатории WordPress с публичным GPL-плагином. Сторонние и продакшн-системы не затрагивались; лаборатория и её каталог кэша удалены после тестирования. Noroxi LLC проводит только авторизованные исследования безопасности и не тестирует системы, тестировать которые не имеет разрешения.

Ссылки

Опубликовано в оборонительных и образовательных целях. Noroxi LLC проводит только авторизованные исследования безопасности; рабочие детали эксплуатации удержаны до появления исправления.

Эксплуатируется на этой неделе · по понедельникам

Новые записи KEV, свежие PoC, разбор WordPress и что сделать на неделе. Без аккаунта.

Адрес используется только для этой рассылки и никому не передаётся.

Следующая статья

Premium Addons for Elementor: нарушение контроля доступа (CWE-862)
Page Optimize: неаутентифицированный DoS через переполнение кэша (CWE-770)