
Пример Seal Security — уязвимое npm-приложение (EJS CVE-2022-29078), исправленное до защищенных версий; интеграция с GitHub Actions и Jenkins
Минимальное, намеренно уязвимое приложение на Node.js/Express, используемое для демонстрации от начала до конца того, как Seal Security устраняет известную CVE путем замены уязвимой зависимости на запечатанную (бэкпортированную, взаимозаменяемую) версию — без изменений ваших объявленных диапазонов версий или кода.
Оно спроектировано как сквозной дымовой тест для Seal CLI в CI/CD: запустите приложение, вызовите реальный эксплойт, запустите Seal и наблюдайте, как тот же самый эксплойт блокируется.
| Экосистема | JavaScript / npm |
| Уязвимый пакет | [email protected] (разрешается в 2.7.4) |
| CVE | CVE‑2022‑29078 — инъекция шаблонов на стороне сервера EJS → удалённое выполнение кода (CVSS 9.8) |
| Запечатанная (исправленная) версия | ejs 2.7.4-sp1 из реестра npm от Seal |
| Интеграция | Seal CLI как один шаг сборки — показано как для GitHub Actions, так и для Jenkins |
Приложение также поставляет другие известные уязвимые зависимости (lodash 4.17.5, json5 0.5.1, got 6.7.1), каждую из которых Seal также исправляет до запечатанной сборки.
Приложение передаёт всю строку запроса URL напрямую в вызов рендеринга EJS:
const data = { name: 'World', ...req.query };
res.render('page', data, ...)
EJS принимает объект settings['view options'], значение outputFunctionName которого записывается — неочищенным — в тело скомпилированной функции шаблона. Поэтому атакующий может внедрить произвольный JavaScript, выполняющийся на сервере с привилегиями процесса Node.js.
Нормальный запрос
/?name=alice
рендерит Hello alice!.
Эксплойт запрос
/?name=Hacker&settings[view%20options][outputFunctionName]=x;setTimeout(function()%7Bprocess.exit(1)%7D,3000);s
Сервер выполняет setTimeout(function(){ process.exit(1) }, 3000). Страница сначала загружается и явно сообщает, что RCE успешно выполнен; перезагрузите через несколько секунд и вы получите ERR_CONNECTION_REFUSED — внедрённый код убил сервер, что доказывает выполнение произвольного кода.
3-секундная задержка намеренная: она позволяет ответу достичь браузера до завершения процесса, чтобы вы увидели страницу «эксплойт выполнен» и затем чистое падение, а не зависшую вкладку.
.
├── index.js # уязвимое приложение Express
├── views/ # шаблоны EJS
├── package.json / package-lock.json
├── Jenkinsfile # пример конвейера Jenkins (Groovy) с этапом Seal
└── .github/workflows/
├── build-and-run.yml # сборка и открытие приложения для тестирования в браузере
└── seal-security.yml # запуск исправления Seal, затем запуск приложения
Seal — это SaaS, размещённый Seal — ничего не устанавливается в вашей среде, и весь трафик — только исходящий HTTPS по TCP 443. Для запуска исправления вам понадобятся:
Настройте их в Settings → Secrets and variables → Actions (GitHub) или Manage Jenkins → Credentials (Jenkins). Никогда не коммитьте токены в репозиторий.
Добавьте в белый список эти хосты Seal для исходящего 443: app.sealsecurity.io, authorization.sealsecurity.io, cli.sealsecurity.io и — для запечатанных пакетов npm — npm.sealsecurity.io. Бинарный файл CLI загружается с github.com / objects.githubusercontent.com.
npm install
npm start # → http://localhost:3001
Откройте http://localhost:3001/?name=alice (работает), затем URL эксплойта выше (приводит к падению сервера).
Seal CLI выполняется как один дополнительный шаг после npm install и до упаковки. Он сканирует разрешённые зависимости и перезаписывает уязвимые на их запечатанные версии, используя удалённый режим исправления (политика управляется централизованно в интерфейсе Seal).
Использует seal-community/cli-action:
- uses: seal-community/cli-action@latest
with:
mode: fix
fix_mode: remote
token: ${{ secrets.SEAL_TOKEN }}
target: package-lock.json # файл блокировки для этой экосистемы
Запустите через Actions → “Seal Security Remediation” → Run workflow. Смотрите .github/workflows/seal-security.yml.
Один добавленный этап после установки и до упаковки. Смотрите Jenkinsfile:
stage('Seal') {
steps {
sh '''
curl -fsSL https://github.com/seal-community/cli/releases/download/latest/seal-linux-amd64-latest -o seal
chmod +x seal
./seal fix --mode remote "$SEAL_MANIFEST" # SEAL_MANIFEST=package-lock.json
'''
}
}
SEAL_TOKEN берётся из учётных данных Jenkins seal-token; установите SEAL_PROJECT в ваш идентификатор проекта Seal.
После seal fix уязвимые зависимости разрешаются в запечатанные сборки из реестра Seal — ваши диапазоны версий package.json остаются прежними:
Запечатанная версия — это тот же пакет с бэкпортированным исправлением безопасности, так что это взаимозаменяемая замена — никаких изменений кода, никакого обновления мажорной версии.
Повторно запустите URL эксплойта против исправленного приложения. Инъекция больше не выполняется: запечатанный ejs отклоняет вредоносный outputFunctionName, и приложение отвечает «Invalid parameter» вместо выполнения полезной нагрузки. Сервер остаётся в рабочем состоянии.
seal fix на конкретный манифест/файл блокировки — package-lock.json для npm. Для репозитория с несколькими манифестами запускайте один seal fix на каждый манифест.Это вся интеграция — один этап, только исходящий трафик, никаких изменений в коде приложения.
| Секрет / учётные данные |
|---|
| Используется для |
|---|
| Куда помещается |
|---|
| Токен Seal | Аутентификация Seal CLI | Секрет GitHub Actions SEAL_TOKEN / учётные данные Jenkins «Secret text» seal-token |
| Токен ngrok (необязательно) | Открытие запущенного приложения для тестирования в браузере | Секрет GitHub Actions NGROK_TOKEN |
| Зависимость | До | После (запечатанная) |
|---|
| ejs | 2.7.4 | 2.7.4‑sp1 |
| lodash | 4.17.5 | 4.17.5‑sp1 |
| json5 | 0.5.1 | 0.5.1‑sp1 |
| got | 6.7.1 | 6.7.1‑sp1 |