Четыре CVE за пять лет в одном npm-пакете, и каждый раз - побег из sandbox с выходом на произвольное выполнение кода на сервере. На пентесте Node.js-бэкенда SaaS-платформы для генерации договоров нашёл в
package-lock.json зависимость angular-expressions@1.4.3 - версия с патчем для CVE-2024-54152, но дырявая для CVE-2026-44643 (патч - 1.5.2). Пакет затянулся транзитивно через docxtemplater. Пользователи загружали Word-шаблоны с placeholder-выражениями, движок компилировал их на сервере без санитизации. Один вредоносный фильтр в выражении - и вместо подстановки {user.name | uppercase} сервер отдаёт содержимое process.env с credentials от PostgreSQL и S3.angular-expressions: серверный движок с клиентской ДНК
Путаница междуangular-expressions и AngularJS (фреймворк 1.x в браузере) - одна из причин, по которой уязвимость долго живёт незамеченной в dependency tree. angular-expressions - standalone npm-модуль от peerigon, который вытащил движок выражений из AngularJS 1.x и адаптировал для серверного использования в Node.js. Это не AngularJS-фреймворк (клиентский MVC, давно EOL) и не Angular 2+ (другая кодовая база). Здесь речь о библиотеке, которая парсит и исполняет выражения вида user.name | uppercase на сервере, внутри Node.js-процесса.Главный потребитель -
docxtemplater, одна из самых популярных библиотек для генерации Word-документов из шаблонов. Встречается и в PDF-генераторах, CMS и внутренних инструментах, где нужна динамическая подстановка данных.Для защиты от выполнения произвольного кода модуль реализует sandbox - блокирует доступ к опасным объектам:
window, process, eval, Function, цепочки [B]proto[/B] и constructor. Проблема в том, что sandbox унаследовал архитектуру из AngularJS 1.x, где его официально признали несостоятельным и удалили в версии 1.6. Как показывает исследование PortSwigger, sandbox AngularJS ломали многократно - от Mario Heiderich в самом начале до серии обходов каждой версии вплоть до 1.5.11.История побегов из sandbox angular-expressions
CVE-2026-44643 - четвёртый задокументированный побег из sandbox в этом пакете:| CVE | GHSA | Год | Вектор |
|---|---|---|---|
| CVE-2020-5219 | GHSA-hxhm-96pp-2m43 | 2020 | RCE через sandbox escape (патч: >= 1.0.1, CWE-74) |
| CVE-2021-21277 | GHSA-j6px-jwvv-vpwq | 2021 | RCE, отдельный путь обхода (патч: >= 1.1.2, CWE-74/CWE-94) |
| CVE-2024-54152 | GHSA-5462-4vcx-jh7j | 2024 | RCE через sandbox escape (патч: >= 1.4.3) |
| CVE-2026-44643 | - | 2026 | RCE через фильтры |
Четыре RCE за пять лет - это не баги, это паттерн. Каждый раз исследователи находят новый путь мимо sandbox (детали раскрыты в GitHub Security Advisories, публичные PoC в Exploit-DB отсутствуют), а мейнтейнеры фиксят конкретный обход, не пересматривая модель. По классификации OWASP уязвимость попадает одновременно под A03:2021 - Injection (CWE-95, eval injection через выражения) и A06:2021 - Vulnerable and Outdated Components (модуль с рецидивирующими критическими уязвимостями).
Анатомия CVE-2026-44643: побег через вредоносные фильтры (CWE-95)
Согласно описанию в NVD, уязвимость позволяет атакующему сформировать выражение с использованием фильтров, которое обходит sandbox и выполняет произвольный код. Затронуты все версииangular-expressions до 1.5.2 (пакет peerigon/angular-expressions). Классификация - CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code, Eval Injection): продукт получает ввод от пользователя, но не нейтрализует синтаксис кода перед использованием в динамическом вызове eval-типа. CWE-95 - подтип CWE-94 (Code Injection), специфичен для языков с динамической оценкой выражений: JavaScript, Python, Perl, PHP. Все они относятся к одному семейству Injection/Code Injection.Фильтры в angular-expressions работают через оператор
|: выражение value | filterName:arg передаёт результат левой части в функцию-фильтр. Внутри парсера фильтры обрабатываются по отдельному пути кода - и именно тут часть ссылок разрешается иначе, чем в основном выражении. Атакующий эксплуатирует эту разницу, чтобы:- Через фильтровый путь получить доступ к запрещённой ссылке, которая в основном выражении была бы заблокирована sandbox
- Восстановить доступ к
Functionчерез цепочку прототипов: любой объект →.constructor(конструктор объекта, напримерString) →.constructor(конструктор конструктора -Function) - Вызвать
Function('return process')()- создать анонимную функцию, возвращающую объектprocessNode.js - Исполнить произвольный код в контексте Node.js - уже за пределами sandbox
JavaScript:
const { compile } = require('angular-expressions');
const scope = { user: { name: 'test' } };
const expr = compile(
"constructor.constructor('return process')() | someFilter // someFilter должен быть зарегистрирован; точный payload не раскрыт в CVE-записи"
);
const result = expr(scope);
// result → объект process Node.js
// → доступ к process.env, require('child_process') и далее
constructor.constructor для обхода sandbox AngularJS описан в исследованиях PortSwigger начиная с 2016 года (в контексте клиентского AngularJS, без публичных PoC для серверного angular-expressions в Exploit-DB). CVE-2026-41468 (Beghelli Sicuro24, CWE-1104) документирует использование того же примитива constructor.constructor в клиентском AngularJS 1.5.2 (embedded EOL-компонент), но в браузерном контексте с MITM-вектором (AV:A, UIРазбор CVSS 4.0 вектора
CVSS 9.3 (CRITICAL) по NVD. Ключевые компоненты вектора:| Компонент | Значение | Интерпретация |
|---|---|---|
| AV:N | Network | Эксплуатация удалённо, через сеть |
| AC:L | Low | Низкая сложность атаки |
| AT:N | None | Нет дополнительных предусловий |
| PR:N | None | Привилегии не требуются |
| UI:N | None | Действие пользователя не требуется |
| VC:H / VI:H / VA:H | High | Полный импакт на конфиденциальность, целостность и доступность уязвимого компонента |
| SC:N / SI:N / SA:N | None | Формально нет воздействия за пределами уязвимого компонента |
SC/SI/SA:N - формально скоуп не меняется, импакт ограничен уязвимым компонентом. На практике скомпрометированный Node.js-процесс почти всегда хранит в
process.env credentials для баз данных, очередей и облачных сервисов, что делает pivot на смежные системы тривиальным. Формальный скоуп CVSS этого не учитывает - поэтому в векторе CVSS 4.0 по NVD 9.3, а cveo.tech публикует 10.0 с SC:H. Расхождение показательное.По оценке CISA (SSVC): Exploitation = none (подтверждённых случаев эксплуатации нет), Automatable = yes (эксплуатация поддаётся автоматизации), Technical Impact = total. EPSS-оценка: 0.0048, перцентиль 0.3808 - вероятность эксплуатации в 30-дневном окне ниже медианы. Но автоматизируемость (Automatable: yes) означает, что появление публичного PoC мгновенно сдвинет этот показатель.
Цепочка эксплуатации SSTI: от fingerprinting до удалённого выполнения кода
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
. Если сервис принимает пользовательские шаблоны (а это его основная функция), атакующий вставляет в DOCX выражение с sandbox escape вместо легитимного placeholder-а. Генератор договоров превращается в точку входа.
Шаг 1 - Fingerprinting. На внешнем пентесте ищу признаки использования
docxtemplater: эндпоинты /api/generate, /api/template, /api/document, наличие DOCX в ответах или в описании API. Поисковые строки в JavaScript-бандлах: docxtemplater, angular-expressions, angularParser. На внутреннем (grey box) - запрашиваю package-lock.json или выполняю npm ls angular-expressions на сервере. Версия ниже 1.5.2 - вектор есть.Шаг 2 - Поиск точки входа. Нужно найти место, где пользовательский ввод попадает в
compile(). Типичные кандидаты: загрузка DOCX-шаблона, параметры API для кастомизации документа, формы с динамической генерацией контента. В Burp Suite перехватываю запросы к эндпоинтам генерации и модифицирую содержимое шаблона.Шаг 3 - Верификация без деструктивных действий. Canary-выражение: подставляю в шаблон
{1+1} и проверяю, возвращается ли 2 в сгенерированном документе. Если да - выражения компилируются и исполняются. Дальше - {user.name.constructor}. Если sandbox работает, выражение вернёт ошибку или пустое значение. Если в ответе function String() { [native code] } - sandbox не блокирует доступ к цепочке прототипов. Можно двигаться дальше.Шаг 4 - Контролируемая эксплуатация. При подтверждённом sandbox escape получаю содержимое
process.env - наименее деструктивная проверка, подтверждающая RCE. Фиксирую для отчёта: payload, скриншот ответа с переменными окружения (замаскировав значения), CVSS-вектор, рекомендации.Ограничения Angular sandbox bypass: когда техника не сработает
Работает если:angular-expressionsверсии < 1.5.2 (прямая или транзитивная зависимость)- Пользовательский ввод достигает вызова
compile()→expr(scope) - Нет WAF/application-level фильтрации (regex-санитизация обходима, см. оговорку ниже)
- Версия
angular-expressions>= 1.5.2 - патч закрывает фильтровый вектор - Выражения захардкожены в коде и пользовательский ввод в них не попадает. Даже при уязвимой версии без контролируемого ввода в
compile()эксплуатация невозможна - Между вводом и
compile()стоит санитизация, блокирующаяconstructor,prototype,[B]proto[/B],Function(. Оговорка: regex-санитизация - пластырь. Как отмечает cveo.tech, история sandbox escape в AngularJS доказывает, что креативные payload-ы обходят наивные regex через Unicode-обфускацию,String.fromCharCode, конкатенацию токенов - Приложение использует Angular 2+ - у него нет
angular-expressionsи нет этого sandbox - Node.js-процесс запущен в изолированном контейнере без сетевого доступа и с read-only FS - RCE формально достигается, но post-exploitation сильно ограничен
npm ls, результат за минуту.Детектирование инъекции шаблонов и митигация (CWE-95)
Аудит зависимостей
Первый шаг - выяснить, есть лиangular-expressions в dependency tree. Команда npm ls angular-expressions 2>&1 | grep -v "deduped" покажет все вхождения, включая транзитивные. Точная загруженная версия: node -e "console.log(require('angular-expressions/package.json').version)". Для yarn и pnpm - yarn why angular-expressions и pnpm why angular-expressions соответственно.Обновление:
npm install angular-expressions@^1.5.2. Если зависимость транзитивная (через docxtemplater), используйте overrides в package.json:
JSON:
{
"overrides": {
"angular-expressions": "^1.5.2"
}
}
docxtemplater бандлят angular-expressions >= 1.5.2 - проверяйте и обновляйте.Мониторинг и сетевые IOC
Если приложение логирует вычисляемые выражения (а если нет - включите), ищите паттерны sandbox escape:
Bash:
# Поиск артефактов sandbox escape в логах приложения
grep -E "constructor|prototype|__proto__|Function\(|globalThis" \
/var/log/app/*.log
# Аудит зависимости
npm ls angular-expressions 2>&1 | grep -v "deduped"
constructor.constructor, [B]proto[/B], Function(, globalThis. Сетевые IOC после успешной эксплуатации: аномальный исходящий трафик от Node.js-процессов, DNS-запросы к неизвестным доменам, подключения к адресам вне allow-листа, скачки CPU (криптомайнинг).Чеклист для пентестера и сисадмина
- Проверить наличие
angular-expressionsв dependency tree командойnpm ls angular-expressions - Определить точную версию - если < 1.5.2, уязвимость подтверждена
- Обновить до
angular-expressions@^1.5.2напрямую или через overrides - Проверить, попадает ли пользовательский ввод в
compile()- без этого условия эксплуатация невозможна даже при уязвимой версии - Если патчинг задерживается - добавить regex-санитизацию как временную меру: блокировать паттерны
constructor,prototype,[B]proto[/B],Function(,eval,globalThis,process,require. Помнить, что это НЕ замена патча - Включить логирование вычисляемых выражений, настроить мониторинг на паттерны escape
- Проверить сетевую изоляцию Node.js-сервера: ограничить исходящие подключения, настроить allow-лист
- Для долгосрочного hardening: не использовать
angular-expressionsдля обработки пользовательского ввода. Для user-supplied шаблонов -isolated-vm, отдельный worker thread или контейнер с ограниченными привилегиями
angular-expressions унаследован от AngularJS 1.x, где разработчики сами признали его несостоятельность и удалили. Но пакет продолжает жить на npm и затягивается в проекты через docxtemplater - зачастую без ведома разработчиков, которые даже не подозревают, что их генератор договоров тянет за собой движок выражений с четырьмя RCE в послужном списке.Паттерн повторяется: команда фиксит конкретный путь обхода, но не пересматривает модель. Sandbox, построенный на блокировке конкретных свойств JavaScript-объектов, обречён - язык слишком динамичен, цепочки прототипов слишком гибки, а парсер выражений слишком сложен для исчерпывающего контроля. Через год-два будет пятый побег. Не потому что мейнтейнеры некомпетентны, а потому что задача «безопасно исполнять произвольные выражения пользователя в том же процессе Node.js» принципиально нерешаема через allow/deny-листы свойств. Единственная надёжная изоляция - процессная или контейнерная, с отдельным рантаймом, без доступа к
process, require и сетевому стеку. Всё остальное - вопрос времени до следующего CVE.