Уязвимость в парсере JSON Oj, который GitLab использует для обработки файлов Jupyter Notebook, даёт возможность аутентифицированному участнику проекта выполнять произвольные команды на уязвимых установках GitLab. Цепочка затрагивает обе редакции - Community Edition и Enterprise Edition - начиная с версии 15.2.0 и до 19.0.1. Разработчики выпустили исправления, обновив Oj до версии 3.17.3.
Детали уязвимостей
Проблема уходит корнями в две давние ошибки безопасности памяти в высокопроизводительном Ruby-парсере Oj, который написан на языке C. Исследовательская команда DepthFirst в рамках инициативы Open Defense проанализировала реализацию Oj на C и выявила 181 приоритезированную уязвимость, из которых 77 относились к классу нарушений безопасности памяти. Две из них - запись за границы стека (stack-based out-of-bounds write) и раскрытие указателя кучи (heap-pointer information disclosure) - были объединены в эксплойт, приводящий к удалённому выполнению кода в GitLab.
Вектор атаки проходит через функциональность отображения различий (diff) для файлов Jupyter Notebook (.ipynb). GitLab использует встроенную библиотеку ipynbdiff для создания читаемых различий. Поскольку файлы notebook представляют собой JSON-документы, ipynbdiff валидирует их с помощью вызова "Oj::Parser.usual.parse". Аутентифицированный пользователь, имеющий право отправлять коммиты и просматривать их diff, может отправить специально сформированное содержимое notebook. Эти данные передаются в нативный парсер Oj, работающий внутри долгоживущего процесса Puma worker.
Первая уязвимость связана с непроверяемой глубиной вложенности в стеке парсера. Oj использует фиксированный стек размером 1024 байта для отслеживания вложенности коллекций. При обработке глубоко вложенных JSON-массивов селекторы типов (0x01 для массива, 0x02 для объекта) перезаписывают память за пределами стека. Это позволяет изменить указатель "buf.head" - стартовый адрес буфера, используемого парсером. Затем, используя подстановку длинного числа, исследователи инициировали вызов "realloc" с поддельным адресом, что в итоге приводило к выделению Ruby-массива поверх освобождённой памяти и перезаписи callback-указателя "p->start".
Вторая уязвимость связана с небезопасным приведением длины JSON-ключа к знаковому 16-битному полю. Если длина ключа превышает 65535 байт, обрезанное значение (например, 29) заставляет парсер использовать встроенное представление ключа, хотя на самом деле был сохранён указатель на динамически выделенную копию. При чтении через встроенное представление возвращается 29 байт, среди которых оказываются 8 байт живого указателя кучи. Эта информация позволяет снизить неопределённость ASLR (рандомизация адресного пространства) и найти базовый адрес библиотек libc и libruby.
Собранная цепочка использует несколько notebook-файлов в одном запросе "diffs_stream". Первый файл (a01) содержит полезную нагрузку для перезаписи callback, за которой следует символ "X", вызывающий исключение. Это оставляет "p->start" с контролируемым адресом. Второй файл (a02) передаёт бинарный триггер, который при следующем вызове "Oj::Parser.usual.parse" перехватывает управление по сохранённому адресу. В зависимости от версии библиотек исследователи использовали gadget в libruby, который загружает адрес команды из буфера и вызывает "system()".
Успешная эксплуатация позволяет выполнять команды от учётной записи "git", под которой работают Puma-воркеры GitLab. В зависимости от изоляции развёртывания это может дать доступ к данным репозиториев, секретам Rails, учётным данным сервисов и внутренним сетевым ресурсам.
Версии GitLab CE/EE с 15.2.0 по 18.10.7, с 18.11.0 по 18.11.4 и с 19.0.0 по 19.0.1 считаются уязвимыми. Первые исправленные версии: 18.10.8, 18.11.5, 19.0.2. Обновление включает Oj версии 3.17.3, в которой устранены обе ошибки. GitLab.com, по заявлению компании, был пропатчен до публичного раскрытия. Администраторам самоуправляемых установок рекомендуется немедленно обновить GitLab до поддерживаемой фиксированной версии. Для установок через Helm, Operator или Docker необходимо проверять версию образа Webservice/Puma, а не только метки чартов.
Помимо двух ключевых уязвимостей, исследование привело к публикации девяти бюллетеней безопасности для Oj, включающих переполнение буфера, использование после освобождения (use-after-free), копирование с отрицательной длиной и целочисленное переполнение. Среди них CVE-2026-54502 (переполнение стека в Oj.dump), CVE-2026-54896 (переполнение кучи при сериализации исключений), CVE-2026-54897 - CVE-2026-54903 (различные use-after-free и целочисленное переполнение).
Этот случай привлекает внимание к проблеме безопасности нативных C-расширений в приложениях на Ruby, таких как GitLab. Даже при использовании языка с автоматическим управлением памятью, low-level зависимости, обрабатывающие данные из ненадёжных источников, могут создавать серьёзные уязвимости. Разработчикам и операторам следует учитывать риски, связанные с native-компонентами, и своевременно обновлять не только само приложение, но и его зависимости. В данном случае уязвимости существовали в Oj почти пять лет, а в GitLab - около четырёх лет, прежде чем были обнаружены и исправлены.
Ссылки
- https://github.com/wupco/gitlab-rce-demo
- https://github.com/ohler55/oj/security/advisories/GHSA-3v45-f3vh-wg7m
- https://github.com/ohler55/oj/security/advisories/GHSA-35w3-pjm6-wj95
- https://github.com/ohler55/oj/security/advisories/GHSA-9ppp-w3g4-fh4q
- https://github.com/ohler55/oj/security/advisories/GHSA-q2gm-54r6-8fwm
- https://github.com/ohler55/oj/security/advisories/GHSA-2cw7-v8ff-p88r
- https://github.com/ohler55/oj/security/advisories/GHSA-9cv6-qcjw-4grx
- https://github.com/ohler55/oj/security/advisories/GHSA-vwm4-62gf-x745
- https://github.com/ohler55/oj/security/advisories/GHSA-m578-w5vf-rfcm
- https://github.com/ohler55/oj/security/advisories/GHSA-475m-ph3x-64gp
- https://depthfirst.com/research/going-depthfirst-achieving-gitlab-rce-via-two-ruby-memory-corruption-vulnerabilities