Security Blog
Posted By Gregory

Under The Hood 9.8.3


English version: Under The Hood 9.8.3


Этот релиз представляет собой цикл технического обслуживания и повышения безопасности, а не запуск новых функций. Большая часть работы была проделана там, где владельцы сайтов редко бывают нужны: слой данных, считывающий ваши журналы безопасности, код, формирующий электронные письма с уведомлениями, и пути экспорта, которые предоставляют вам CSV-файл с записями вашей активности. Ничто из этого не меняет внешний вид плагина, но все это меняет его надежность и безопасность при высокой нагрузке на сайт, при большом объеме журнала или при попытке злоумышленника исследовать нерешенную нами проблему.

Ниже представлено описание процесса внедрения для администраторов и разработчиков, которые хотят понять, что именно изменилось.

Закрытие вектора внедрения адреса получателя электронного письма

WP Cerber отправляет два типа транзакционных писем, содержащих имя пользователя: сообщение с PIN-кодом для двухфакторной аутентификации и уведомление об активности. В обоих случаях получатель формируется в привычном формате RFC 5322: Name <email> , и в обоих случаях это имя берется непосредственно из полей профиля WordPress ( user_firstname , user_lastname и display_name ) без экранирования.

Проблема заключается в том, как wp_mail() обрабатывает строковый адрес получателя. Она разделяет строку по буквальной запятой и не учитывает границы строк в кавычках. Пользователь, который вставил правильные символы в свое имя профиля, может, таким образом, добавить дополнительный адрес в список получателей электронного письма с PIN-кодом двухфакторной аутентификации, создаваемого функцией CRB_2FA::send_user_pin() , и в список получателей оповещений, создаваемых функцией cerber_get_email() , входящей в ветвь user_list . На сайте, где любой может зарегистрироваться и установить отображаемое имя, это реальный способ незаметно перенаправить копию сообщения безопасности.

Мы добавили специальный фильтр, crb_sanitize_mail_display_name() , в cerber-common.php и применили его в обоих местах вызова. Он удаляет только те символы, которые важны для этого класса инъекций: запятую, кавычки, обратную косую черту, угловые скобки и управляющие символы. Видимое имя по-прежнему отображается нормально для легитимных пользователей, а список получателей больше нельзя изменить с помощью специально созданного поля профиля.

Удаление сохраненного вектора XSS из сканера прав собственности

Сканер вредоносных программ сообщает об изменении владельца установленного плагина в репозитории WordPress.org. Для этого он использует метаданные о владельце, предоставленные этим репозиторием, и до настоящего момента встраивал часть этих метаданных в уведомление административной панели в виде необработанного HTML-кода, включая созданные вручную теги привязки для ссылок на профили владельцев.

Этот репозиторий является надежным источником, и вероятность появления вредоносных метаданных низка, но внешние данные никогда не должны попадать в административный интерфейс в виде незашифрованной разметки. Рассматривать его как надежный источник — это именно то предположение, на которое мы предпочитаем не полагаться.

Чтобы корректно закрыть вектор, а не вносить в него изменения локально, мы ввели новый элемент UI Factory, formatted_text , создаваемый с помощью вспомогательной функции crb_ui_formatted_text() . Он отображает шаблон в виде простого текста, содержащий пронумерованные заполнители %N$s . Литеральные фрагменты шаблона и скалярные аргументы экранируются HTML-кодом, в то время как любой аргумент, являющийся элементом пользовательского интерфейса, отображается активным рендерером. Таким образом, каждое динамическое значение экранируется в контексте, в котором оно фактически генерируется, что является единственным надежным способом предотвращения ошибок контекста вывода.

В функции crb_check_ownership() сообщение о владельце теперь использует этот элемент, а ссылки на профили владельцев формируются с помощью crb_ui_link() вместо объединенных строк привязки, поэтому URL-адреса профилей и отображаемые имена экранируются как URL-адреса и как текст соответственно. Строка перевода остается неизменной, поэтому локализация продолжает работать. Риск XSS-атаки на сохраненные административные профили устранен.

Исправление фильтра «Любая программная ошибка» в Traffic Inspector.

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

Первопричиной была проблема с приоритетом операторов при формировании предложения WHERE. В устаревшем коде фрагменты фильтра объединялись в строки, а условие групповой ошибки не заключалось в скобки, поэтому оператор OR внутри него мог выйти за рамки окружающей логики AND . В результате переноса запроса трафика в построитель запросов условие групповой ошибки теперь корректно заключено в скобки, и комбинированный фильтр работает так, как и предполагалось. При сужении поиска результаты теперь учитывают все заданные условия.

Соответствующие поисковые подстановочные знаки буквально

Ранее расширенный поиск по логам трафика позволял символам % или _ , введенным в поисковый запрос, действовать как метасимвол LIKE , поскольку запрос помещался в шаблон без экранирования. В лучшем случае это случайная особенность, а в худшем — ненужный шаблон загрузки.

Теперь каждый LIKE запрос проходит через CRB_Database::escape_like() перед добавлением окружающих символов подстановки, поэтому % и _ сопоставляются с буквальными символами, введенными пользователем. Поиск работает предсказуемо, и запрос не может быть скорректирован таким образом, чтобы сканировать больше данных, чем предполагалось.

Исправление обработки ограничений памяти при экспорте

Экспорт больших журналов активности или трафика требует значительных ресурсов памяти, поэтому WP Cerber увеличивает объем доступной памяти перед операцией. В некоторых средах числовое значение ограничения памяти, например 512 интерпретировалось как байты, а не мегабайты. В таких случаях плагин не увеличивал лимит должным образом, и экспорт мог остановиться раньше, чем ожидалось.

Теперь значение интерпретируется с использованием правильной единицы измерения, поэтому увеличение объема памяти применяется в соответствии с задуманным, и крупные экспорты завершаются успешно в средах, которые ранее были затронуты.

Восстановление экспорта логов в потоковом режиме

В экспорте данных об активности и трафике для чтения соответствующих строк использовались фрагменты, при этом для каждого фрагмента повторно выполнялся тот же SELECT с возрастающим OFFSET . В больших массивах данных это приводит к сканированию с глубоким смещением, где каждый фрагмент обходится дороже предыдущего, и результат в памяти постоянно увеличивается.

Теперь оба экспорта считывают свои строки за один проход без буферизации через CRB_Database::query_stream() , обернутый в генератор, который выдает по одной строке за раз. Память остается неизменной независимо от количества совпадающих записей, и база данных выполняет работу один раз, а не один раз за блок. Поскольку поток без буферизации блокирует соединение во время его обработки, общее количество строк и диапазон дат определяются заранее с помощью отдельных буферизованных запросов COUNT и MIN / MAX , построенных на основе тех же фильтров, прежде чем поток будет открыт.

В функцию crb_file_headers() , предназначенную для загрузки файлов, были добавлены два заголовка ответа. X-Accel-Buffering: no указывает Nginx, при работе с PHP-FPM, пересылать каждый фрагмент немедленно, вместо буферизации всего экспорта перед отправкой, что улучшает время до первого байта и предотвращает хранение большого CSV-файла в памяти прокси-сервером. Этот заголовок является специфичным для Nginx и безвредно игнорируется Apache с mod_php и другими прокси-серверами. Cache-Control: no-store предотвращает кэширование браузером или любым промежуточным прокси-сервером конфиденциального экспорта журналов безопасности.

Мы также сделали очистку потока детерминированной. Экспорт обрабатывает генератор внутри блока try / finally и освобождает единственный оставшийся дескриптор в блоке finally , поэтому небуферизованный результат освобождается, а соединение разблокируется на каждом пути выхода: при обычном завершении, преждевременной остановке или исключении, возникшем во время записи строки. Ранее преждевременная остановка или ошибка до полного потребления могли оставить результат открытым, а соединение заблокированным, что приводило к сбою следующего запроса в этом потоке.

Отображение экспортированного диапазона дат.

В заголовке CSV-файла, экспортируемого ранее, отображались только активные фильтры. Теперь в заголовке для экспорта данных об активности и трафике добавляются две строки, показывающие самые старые и самые новые временные метки записей, охватываемые экспортируемыми данными. Поскольку заголовок записывается перед первой строкой, диапазон берется из соответствующего запроса MIN / MAX , построенного на основе того же запроса с фильтрами, что и экспорт, и опускается, если ни одна строка не соответствует запросу. При архивировании экспорта файл теперь записывает точное временное окно, которое он представляет.

Делать экспортные сбои видимыми, а не замалчиваемыми.

Старый способ экспорта мог незаметно завершиться с ошибкой. Если база данных была недоступна или поток строк не удавалось открыть, предыдущий код, как правило, создавал пустой CSV-файл без объяснений, что является наихудшим результатом для того, кто пытается получить записи во время инцидента.

Пути чтения и экспорта для обоих типов журналов были переработаны таким образом, чтобы возвращать результат Revalt , содержащий либо полезную нагрузку данных, либо структурированную ошибку с различными кодами, такими как activity_export_query_build_failed , activity_export_db_unavailable и activity_export_stream_failed . Сбой нижнего уровня добавляется в результат, так что исходная причина сохраняется, а не отбрасывается. Сбои при настройке теперь прерывают экспорт с помощью wp_die() до отправки хотя бы одного байта CSV, вместо потоковой передачи пустого файла.

Когда экспорт завершается неудачей, администратор, обладающий правами manage_options видит в сообщении указанную цепочку причин, например, основную ошибку базы данных. Пользователи без этих прав не видят этой информации, поэтому подробные сведения доходят до тех, кто может принять меры, в то время как внутренние особенности базы данных остаются недоступными для остальных. Сообщение экранируется с помощью функции crb_escape_html() .

Объединение журналов активности и трафика в предметные классы.

Значительная часть этого цикла носила структурный характер. В журналах активности и трафика строки SQL и обработка результатов были разбросаны по всему коду панели мониторинга и экспорта. Мы перенесли эту логику в классы CRB_Activity и CRB_Traffic_Log , поэтому построение запросов и извлечение строк теперь осуществляются через понятные методы, такие как fetch() и stream_log() а не передаются через код представления в виде необработанного SQL-кода.

Теперь оба класса формируют свои запросы с помощью построителя запросов DB Warp, получаемого через warp_get_db() вместо того, чтобы вручную объединять фрагменты WHERE, JOIN и LIMIT. Это важно не только с точки зрения аккуратности. Передача каждого предоставленного пользователем значения фильтра через один слой экранирования устраняет прежнее сочетание ручного экранирования, $wpdb->prepare() и конкатенации в кавычках, что как раз и является тем видом несоответствия, который скрывает ошибки внедрения зависимостей. Когда неисправный слой базы данных не может построить запрос, код теперь безопасно завершается с ошибкой отсутствия совпадения, а не выполняет запрос без фильтра.

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

В процессе извлечения кода оповещения обнаружены две скрытые ошибки.

Перемещение отправки оповещений администратора из CRB_Activity::log() в отдельный класс CRB_Activity_Alerts выявило две существовавшие ранее ошибки, вызванные смещением числовых индексов позиционного массива.

Первая проблема затронула ссылки на панели управления внутри электронных писем с оповещениями. Сопоставление разреженных ключей и плотных значений привело к смещению значений, так что параметр ссылки, такой как filter_ip мог получать начало диапазона IP-адресов вместо нужного адреса. Сопоставление значений с именованными ключами исправило выравнивание, и теперь ссылки в электронных письмах с оповещениями указывают туда, куда нужно.

Вторая проблема связана с поиском пользователя по строке поиска. Код вызывал функцию wp_get_current_user() , которая возвращает объект WP_User даже для пользователя с ID 0, поэтому при сопоставлении использовалась неверная идентификация. Теперь поиск пользователя для события осуществляется с помощью crb_get_userdata() , и система предотвращает отсутствие пользователя. Теперь оповещения, соответствующие пользователю, соответствуют правильному пользователю.

Более тихая панель управления: приоритет оператора при проверке изменений.

CRB_Activity::is_modified_since() сравнивала метку времени, используя $stamp < $status['data_modified'] ?? PHP_INT_MAX . Приоритет операторов определяет это как (... < ...) ?? PHP_INT_MAX , что делает часть, отвечающую за объединение с нулевыми значениями, мертвым кодом и вызывает предупреждение о неопределенном ключе всякий раз, когда data_modified отсутствует. Теперь объединение заключено в скобки, поэтому отсутствующая метка модификации рассматривается как "измененная", что соответствует соседнему методу is_modified() , и ложное предупреждение исчезает.

Централизация определений схемы базы данных

Последний структурный элемент — это поддержка схемы. В коде установки и обновления для таблиц журналов использовался встроенный SQL-запрос CREATE TABLE , что означало, что одна и та же таблица могла быть описана в нескольких местах. Теперь эти объявления поступают из одного источника, CRB_Schema_Definitions , поэтому установка и обновление работают на основе одного канонического определения, и CRB_Schema_Manager может более последовательно обнаруживать изменения схемы в существующих установках. Аналогичным образом, SQL-запрос DROP INDEX был заменен на CRB_Schema_Manager::drop_index_if_exists() .

Мы также сделали декодирование сохраненных данных полей запроса более щадящим. Теперь устаревшие значения, допускающие значение null, пустые значения, недопустимый JSON и неподдерживаемые сериализованные данные обрабатываются одинаково безопасным способом: вместо того, чтобы позволить некорректной записи нарушить чтение, происходит преобразование в пустой массив.

Почему этот релиз важен

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


I'm a team lead in Cerber Tech. I'm a software & database architect, WordPress - PHP - SQL - JavaScript developer. I started coding in 1993 on IBM System/370 (yeah, that was amazing days) and today software engineering at Cerber Tech is how I make my living. I've taught to have high standards for myself as well as using them in developing software solutions.

View Comments
There are currently no comments.