YApi — платформа документации API, мок-сервера и тестов с 27 тысячами звёзд — не обновлялась с 2022 года. На новом Node.js у неё ломаются токены, клиент не собирается, нужное ей расширение браузера больше не работает в Chrome, а мок-скрипты позволяют выполнить код на сервере.Я продолжил проект и по дороге нашёл, что без настройки passsalt токены проектов шифруются общеизвестным ключом — участник проекта может выдать себя за другого пользователя. В статье — как это исправлено без поломки старых токенов, песочница для скриптов на isolated-vm, своё расширение на Manifest V3 и тест обновления на настоящей базе YApi. Читать дальше
Уровень сложностиСредний
Время на прочтение9 мин
Охват и читатели8.4K
Кейс
YApi — платформа управления API от команды YMFE (Qunar): документация интерфейсов, мок‑сервер на Mock.js, тестовые наборы с проверками и отчётами, импорт Swagger и Postman. Её ставят на свои серверы, чтобы бэкенд, фронтенд и тестировщики работали с одним описанием API. У проекта 27,7 тысячи звёзд на GitHub, а неофициальный Docker‑образ скачали больше 600 тысяч раз. В Китае это стандартный инструмент — примерно как у нас связка Swagger и Postman.
Последний релиз, 1.12, вышел в ноябре 2022 года. С тех пор в репозитории висят 1629 открытых issue. Среди них отчёт об удалённом выполнении кода через мок‑скрипты, на который никто не ответил, и десятки вопросов «cross‑request больше не ставится, что делать?».
Я взялся продолжить проект под именем Yapix (GitHub, Apache-2.0). Он работает на существующей базе YApi, а обновление с настоящей базы YApi 1.12 проверяется в CI на каждое изменение. В статье — что сломалось за три года, что нашлось в коде по дороге и как это чинилось.
Что сломано в 2026 годуПроект, который никто не трогает, ломается не сам — ломается мир вокруг.
Node.js. YApi шифрует токены проектов через crypto.createCipher. В Node.js 22 эту функцию удалили. На новом Node сервер стартует, но всё, что связано с токенами (открытый API, автотесты из CI, плагины для IDE), перестаёт работать.
Сборка клиента. Фронтенд собирается ykit — обёрткой над webpack 1 — и node‑sass 4. Ни то, ни другое на современном Node не ставится. Собранный бандл лежит прямо в репозитории, так что YApi можно запустить, но нельзя поменять в интерфейсе ни строчки.
npm‑зеркала. package-lock.json ссылается на registry.npm.taobao.org и registry.nlark.com. Оба адреса больше не отдают пакеты: npm ci падает на первом же пакете. Лечится переписыванием адресов на registry.npmjs.org, хэши при этом остаются верными — тарболы те же.
Расширение браузера. Кнопка «运行» (запуск запроса) и тестовые наборы в браузере работают через расширение cross‑request: страница не может сама отправить запрос на чужой домен из‑за CORS. Расширение написано под Manifest V2, который Chrome больше не запускает. Из Chrome Web Store его давно убрали, лицензии у него нет.
Зависимости. По базе GitHub Advisory в production‑зависимостях YApi 1.12 — 244 известные уязвимости, из них 50 критических. Среди них mongoose 5.7, vm2 и старый koa.
Токены, которые можно подделатьРазбираясь с createCipher, я прочитал, как вообще устроены токены проектов. Токен нужен для доступа к API без входа: его копируют в CI, в плагин IDE, в генератор TypeScript‑типов.
В базе у каждого проекта хранится случайный токен из 20 символов. Пользователю выдаётся не он, а зашифрованная строка uid|токен_проекта: так сервер знает, от чьего имени пришёл запрос, и применяет права этого пользователя. Ключ шифрования берётся из passsalt в config.json, а если его там нет — из константы в коде:
const defaultSalt = 'abcde';
passsalt нет ни в примере конфига, ни в инструкциях по установке. Значит, почти во всех установках ключ — пять букв, которые лежат на GitHub.
Последствия прямые. Любой, у кого есть хотя бы гостевой доступ к проекту, получает свой токен. Раз ключ известен, по токену можно получить токен проекта и собрать токен с чужим uid: сервер проверяет только, что строка расшифровалась, и работает с правами указанного пользователя.
Как это исправлено в Yapix:
если в конфиге нет своего passsalt, при первом запуске генерируется случайный секрет на 32 байта. Он хранится в базе, в отдельной коллекции;
токены, которые расшифровываются только общеизвестным ключом, по умолчанию отклоняются с понятным сообщением «получите новый токен в настройках проекта»;
на время миграции их можно временно включить ("legacyTokens": true), и каждое такое использование пишется в лог;
если passsalt в конфиге был задан, старые токены продолжают работать как есть.
Тем, кто остаётся на YApi, достаточно задать длинный случайный passsalt в config.json и перевыпустить токены.
Заодно выяснилось, что /api/project/token не проверял, имеет ли пользователь доступ к проекту: токен выдавался по любому project_id. Теперь проверяет.
Токены, выданные с настоящим passsalt, должны продолжить работать. crypto.createCipher('aes192', password) превращает пароль в ключ и IV через функцию OpenSSL EVP_BytesToKey: MD5 в один проход, без соли. Её несложно повторить:
function deriveKeyAndIv(password) {
const pass = Buffer.from(password, 'utf8');
const parts = [];
let prev = Buffer.alloc(0);
while (Buffer.concat(parts).length < 24 + 16) {
prev = crypto.createHash('md5').update(Buffer.concat([prev, pass])).digest();
parts.push(prev);
}
const bytes = Buffer.concat(parts);
return { key: bytes.subarray(0, 24), iv: bytes.subarray(24, 40) };
}
Дальше — обычный createCipheriv('aes-192-cbc', key, iv). Я сравнил результат с настоящим createCipher на Node 20 для нескольких паролей, включая кириллицу, — побайтно совпадает.
В YApi можно писать JavaScript в трёх местах: мок‑скрипт меняет ответ мок‑сервера, скрипт проверки в тестовом наборе проверяет ответ, а пред‑ и постскрипты запроса выполняются при автотестах. Всё это выполнялось на сервере: мок‑скрипты — через safeify (обёртку над vm2), проверки и автотесты — через встроенный node:vm.
Документация Node прямо говорит, что node:vm — не механизм безопасности. vm2 заброшен автором после серии обходов песочницы. Любой, кто может редактировать мок‑скрипт, а при открытой регистрации это кто угодно, может выполнить код на сервере. Об этом и говорит тот самый открытый отчёт.
В Yapix скрипты выполняются в isolated‑vm — отдельном изоляте V8 со своей кучей. Устройство такое:
Новый изолят на каждый запуск. Лимит памяти — 64 МБ. После выполнения изолят уничтожается, так что запуски не видят друг друга.
Внутрь передаются только данные. Объекты (mockJson, params, body, header) копируются как JSON. Функций хоста внутри нет вообще: ни require, ни process, ни файловой системы, ни сети.
Всё, чем скрипты пользовались, живёт внутри изолята. Это assert, log, Mock и Random, utils с хэшами, base64, CryptoJS и jsrsasign, а также storage. Библиотеки грузятся, только если скрипт их упоминает. Скомпилированный код кэшируется, и повторная загрузка Mock.js занимает миллисекунды.
Результат возвращается обратно как JSON. Сервер берёт из него только нужные поля.
Время ограничено. Синхронная часть — 3 секунды процессорного времени, всё выполнение — 10 секунд. По дедлайну изолят уничтожается, даже если скрипт ждёт промис.
Интерфейс для скриптов не поменялся. Скрипты, которые писали под YApi, работают без правок, если не лезли в Node.js — а туда им лезть и не следовало. Проверки из тестовых наборов по‑прежнему пишутся через assert.equal(status, 200): модуль assert я реализовал внутри изолята, с теми же сообщениями об ошибках.
Слой данных пришлось переписать почти целиком, хотя кода там немного. Вот что поменялось между версиями:
Model.remove() и Model.update() удалены. Важно, что remove удалял все подходящие документы, а update без multi: true — только один. Значит, замены такие: deleteMany и updateOne. Где стоял multi, там updateMany.
Колбэки убраны отовсюду. Плагин mongoose-auto-increment, который выдаёт числовые id (у всех сущностей YApi id числовые), был целиком на колбэках. Я переписал его на промисы. Он пользуется той же коллекцией счётчиков, что и оригинал, поэтому новые id продолжают старые последовательности.
strictQuery, useFindAndModify и прочие флаги сменили значения по умолчанию. Их пришлось выставить явно, чтобы поведение запросов не поменялось.
В YApi параметры запроса часто попадают в фильтры MongoDB как есть. Если вместо строки прислать объект {"$ne": null}, поиск превращается в запрос с оператором. В Yapix все запросы к API проходят общий обработчик, и параметры с операторами MongoDB отклоняются там с кодом 400. $ref и $schema из JSON Schema этим не задеваются: блокируется только список настоящих операторов.
Первый администратор в YApi создавался с паролем ymfe.org, и он был напечатан в документации. Теперь пароль берётся из YAPIX_ADMIN_PASSWORD или генерируется и показывается один раз.
Пароли хранились как SHA-1 с солью. Теперь это scrypt. Старые хэши заменяются при следующем входе пользователя, и ничего делать не нужно.
Вход через LDAP подставлял логин в фильтр поиска без экранирования и принимал пустой пароль. Многие LDAP‑серверы считают пустой пароль анонимным входом и пускают. Теперь логин экранируется по RFC 4515, а пустой пароль сразу отклоняется.
Сборку я заменил на webpack 5, Babel 7, less 4 для темы antd и dart‑sass. Первый собранный бандл открылся белым экраном с ошибкой exports is not defined.
Оказалось, в коде YApi во многих файлах import соседствует с module.exports. Старый Babel с пресетом es2015 превращал всё в CommonJS, и такое смешение работало. Webpack 5 видит import и считает файл ES‑модулем, а в ES‑модуле нет module и exports. Помогло то же, что делал старый Babel: переводить весь свой код в CommonJS (modules: 'commonjs' плюс sourceType: 'unambiguous').
Интерфейс остался на antd 3 и React 16. Переход на актуальный antd означает переписать весь интерфейс, это отдельная большая работа. Но и так нашлось что убрать. При каждом открытии страницы администратором клиент ходил за списком версий на сторонний мок‑сервис fastmock.site. Для интранет‑установок это лишний запрос наружу, так что баннер обновлений я удалил.
Своё расширение вместо cross‑requestКонтракт расширения простой: на странице должна появиться функция window.crossRequest(options), которая отправляет запрос в обход CORS и вызывает success(body, headers, data) или error(...). Я написал расширение с нуля под Manifest V3, опираясь только на то, как его вызывает код YApi. Код cross‑request без лицензии я не открывал.
Устроено оно из трёх частей:
page.js в мире страницы определяет window.crossRequest и отправляет запрос через postMessage;
bridge.js в изолированном мире расширения передаёт его в service worker;
service worker делает fetch. У расширения есть host permissions, поэтому CORS его не ограничивает.
Главное отличие от оригинала — модель доверия. Функция, которая шлёт запросы с Cookie пользователя на любой адрес и возвращает ответы, — опасная вещь для любой страницы, где она есть. Поэтому скрипты регистрируются динамически (chrome.scripting.registerContentScripts) и только для сайтов, которые пользователь сам разрешил во всплывающем окне. Service worker ещё раз сверяет origin отправителя со списком, принимает только http(s) и ограничивает таймаут.
Раз контракт тот же, расширение работает и с оригинальным YApi 1.12. Я проверил это тем же автотестом: Chrome for Testing загружает расширение, разрешает сайт и нажимает «发送» на странице интерфейса.
Уязвимость, которую нельзя обновитьПосле обновлений в production‑зависимостях осталась одна запись: prototype pollution в Mock.js. Исправленной версии нет — 1.1.0 последняя. А Mock.js — сердце мок‑сервера, шаблоны для него пишут пользователи.
Уязвимость в функции Util.extend, которая рекурсивно копирует шаблон. Ключ proto из JSON‑шаблона приводит её к Object.prototype, и дальше она пишет туда. Внутри Mock.js эта функция вызывается через общий объект Util. Поэтому хватает одного модуля‑обёртки, который подменяет Util.extend на копию, пропускающую proto. Все места, где подключался mockjs, теперь подключают обёртку. В тестах есть проверка, что шаблон с proto больше не загрязняет прототип.
Главное обещание форка — «ваша база продолжит работать». Поэтому в CI есть отдельная задача:
Из истории репозитория берётся последний коммит YMFE (git archive), зеркала в lock‑файле переписываются на npmjs, запускается оригинальный YApi 1.12 на Node 20.
Через его API создаются пользователь, группа, проект, интерфейсы, тестовый набор и токен.
YApi останавливается, на той же базе запускается Yapix на Node 24.
Тест проверяет, что пользователь входит со старым паролем, данные на месте и мок отвечает. Новые id должны продолжать старые последовательности, старый токен — отклоняться с понятной ошибкой, а новый — работать. Затем всё то же повторяется с legacyTokens.
Ещё этот тест подтвердил неприятную деталь, которую я вписал в инструкцию по обновлению: после входа в Yapix пароль пересчитывается в scrypt, и вернуться на YApi без сброса паролей уже не получится.
ИтогYApi 1.12 | Yapix 2.0 | |
|---|---|---|
Node.js | до 20 | 24 LTS |
Скрипты | vm2/safeify, node:vm | отдельный изолят V8 с лимитами |
Токены без | подделываются | случайный ключ, старые отклоняются |
Уязвимости в production‑зависимостях | 244 (50 критических) | 1, закрыта обёрткой |
Сборка клиента | не собирается | webpack 5 |
Расширение | MV2, не работает | MV3, только на разрешённых сайтах |
Попробовать можно в Docker (образ собран под amd64 и arm64):
curl -O https://raw.githubusercontent.com/Perruer/yapix/main/docker-compose.yml
YAPIX_ADMIN_PASSWORD='длинный пароль' docker compose up -d
Если у вас работает YApi, прочитайте UPGRADING.md: там про версии MongoDB (нужна 4.4+), замену токенов и ограничения скриптов. Буду рад issue и отзывам — особенно от тех, кто переносит живую установку.
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Как я научил Claude заказывать продукты в Яндекс Лавке (и что для этого пришлось отреверсить) | 0 | 10.18 | 22-07-2026 |
| 2 | Назад в 2005-й. API-first как третья пилюля от деградации проекта под LLM | 0 | 7.47 | 26-07-2026 |
| 3 | Баги на диком западе: топ-10 ошибок в C и C++ проектах за 2025 год | 0 | 8.06 | 30-12-2025 |
| 4 | Вторая копия Vue: как лишняя строка в lockfile повесила Chromium | 0 | 6.77 | 09-08-2026 |
| 5 | Какие наши продукты задевает эта CVE? Я продолжил заброшенный Minefield и нашёл, что он читал SBOM задом наперёд | 0 | 9 | 26-09-2026 |
| 6 | [Перевод] От тестирования релиза с высоким уровнем риска к новому ИИ-инструменту для QA | 0 | 11.89 | 26-09-2026 |
| 7 | Формула «идеального enterprise» для open-source | 0 | 18.47 | 12-08-2026 |
| 8 | Ваши тесты упали по причине JavaScript | 0 | 11.77 | 17-11-2025 |
| 9 | Организовал весь пентест-арсенал в одном месте: всё под рукой, офлайн и на русском | 5 | 7 | 28-06-2026 |