Skip to content

Comments

chore(lambda-nodejs): input sanitisation for bundling options#37019

Draft
vishaalmehrishi wants to merge 14 commits intomainfrom
mehrishi/input-sanitisation-bundling-options
Draft

chore(lambda-nodejs): input sanitisation for bundling options#37019
vishaalmehrishi wants to merge 14 commits intomainfrom
mehrishi/input-sanitisation-bundling-options

Conversation

@vishaalmehrishi
Copy link
Contributor

Issue # (if applicable)

Closes #.

Reason for this change

Description of changes

Describe any new or updated permissions being added

Description of how you validated changes

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@aws-cdk-automation aws-cdk-automation requested a review from a team February 18, 2026 12:53
@github-actions github-actions bot added the p2 label Feb 18, 2026
@vishaalmehrishi vishaalmehrishi changed the title Mehrishi/input sanitisation bundling options chore(aws-lambda-nodejs): input sanitisation for bundling options Feb 18, 2026
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Feb 18, 2026
Copy link
Collaborator

@aws-cdk-automation aws-cdk-automation left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This review is outdated)

@vishaalmehrishi vishaalmehrishi changed the title chore(aws-lambda-nodejs): input sanitisation for bundling options chore(lambda-nodejs): input sanitisation for bundling options Feb 18, 2026
@aws-cdk-automation aws-cdk-automation dismissed their stale review February 18, 2026 12:57

✅ Updated pull request passes all PRLinter validations. Dismissing previous PRLinter review.

@aws-cdk-automation aws-cdk-automation added pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. and removed pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. labels Feb 18, 2026
@vishaalmehrishi vishaalmehrishi force-pushed the mehrishi/input-sanitisation-bundling-options branch from eabb485 to 9b5fd2d Compare February 18, 2026 14:18
@aws-cdk-automation aws-cdk-automation added pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. and removed pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. labels Feb 18, 2026
@vishaalmehrishi vishaalmehrishi force-pushed the mehrishi/input-sanitisation-bundling-options branch from 496ad55 to de35360 Compare February 19, 2026 13:49
…ling

Refactored local bundling to execute esbuild via spawnSync with shell=false,
passing arguments as an array instead of interpolating into a shell command
string. For Docker bundling, all esbuild arguments are shell-escaped before
interpolation into the bash -c command string.

- Added execDirect() and shellEscapeForBundlingCommand() utilities
- Extracted buildEsbuildArgs() for reuse across both bundling paths
- Added runBinCommandAsArray() to PackageManager
- Refactored toCliArgs() to return string[] without shell quoting
- Refactored getLocalBundlingProvider() into 3 phases (hooks/tsc, esbuild, deps)
- Added comprehensive tests for shell metacharacter handling
* Spawn sync with error handling.
*
* NOTE: This function is typically used with a shell interpreter (bash/cmd) as the command,
* which means the args are interpreted by the shell. When user input is involved,

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.

Copilot Autofix

AI 4 days ago

General approach: Avoid giving the shell a command string containing unescaped, user‑influenced values. Either pass arguments directly as an array (bypassing the shell) or, if we must use bash -c / cmd /c, rigorously escape any data that can contain special characters before embedding it into the command string.

Best fix here: We already have shellEscapeForBundlingCommand(value, platform) and we already use it for the esbuild command. The remaining surface that CodeQL traces into is the OsCommand helper plus the chain combination used to build depsCommand and then localCommand. The tainted data are:

  • filePath, src, dest, dir path arguments in OsCommand methods.
  • data in OsCommand.write/writeJson (JSON of dependencies built from extractDependencies).
  • The resulting per-step command strings combined by chain and finally passed as localCommand to bash -c / cmd /c.

We can fix this without altering observable behavior by:

  1. Making OsCommand build its commands using shellEscapeForBundlingCommand for both Unix and Windows paths/data instead of naive "${...}" or raw '${data}'. This prevents quotes, semicolons, &&, etc., in file paths or JSON data from breaking out of their intended position in the command.
  2. Leaving chain as a simple join, but ensuring that every command fragment it receives is already correctly shell‑escaped by construction. Since depsCommand elements come exclusively from OsCommand and packageManager.installCommand.join(' ') (controlled by the library), tightening OsCommand is sufficient for the tainted flow CodeQL identified.

This keeps the overall structure the same: we still generate one string localCommand and pass it to bash -c/cmd /c, but now all user‑influenced values embedded in that string are safely escaped, so the “shell command built from environment values” issue is mitigated.

Concretely:

  • In packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts, within the OsCommand class, update all string templates that interpolate filePath, src, dest, dir, or data to first run them through shellEscapeForBundlingCommand(…, this.osPlatform) and then use those escaped values. For Unix, this replaces manual '${data}' quoting with robust escaping; for Windows, it protects paths and content when invoking via cmd /c. No other files need changes.
Suggested changeset 1
packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts b/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts
--- a/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts
+++ b/packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts
@@ -431,14 +431,19 @@
   constructor(private readonly osPlatform: NodeJS.Platform) {}
 
   public write(filePath: string, data: string): string {
+    const escapedFilePath = shellEscapeForBundlingCommand(filePath, this.osPlatform);
+    const escapedData = shellEscapeForBundlingCommand(data, this.osPlatform);
+
     if (this.osPlatform === 'win32') {
+      // On Windows, preserve the existing semantics for empty data vs non-empty data,
+      // but ensure the file path and data are safely escaped for cmd.
       if (!data) { // if `data` is empty, echo a blank line, otherwise the file will contain a `^` character
-        return `echo. > "${filePath}"`;
+        return `echo. > ${escapedFilePath}`;
       }
-      return `echo ^${data}^ > "${filePath}"`;
+      return `echo ${escapedData} > ${escapedFilePath}`;
     }
 
-    return `echo '${data}' > "${filePath}"`;
+    return `echo ${escapedData} > ${escapedFilePath}`;
   }
 
   public writeJson(filePath: string, data: any): string {
@@ -447,32 +447,41 @@
   }
 
   public copy(src: string, dest: string): string {
+    const escapedSrc = shellEscapeForBundlingCommand(src, this.osPlatform);
+    const escapedDest = shellEscapeForBundlingCommand(dest, this.osPlatform);
+
     if (this.osPlatform === 'win32') {
-      return `copy "${src}" "${dest}"`;
+      return `copy ${escapedSrc} ${escapedDest}`;
     }
 
-    return `cp "${src}" "${dest}"`;
+    return `cp ${escapedSrc} ${escapedDest}`;
   }
 
   public changeDirectory(dir: string): string {
-    return `cd "${dir}"`;
+    const escapedDir = shellEscapeForBundlingCommand(dir, this.osPlatform);
+    return `cd ${escapedDir}`;
   }
 
   public remove(filePath: string, force: boolean = false): string {
+    const escapedFilePath = shellEscapeForBundlingCommand(filePath, this.osPlatform);
+
     if (this.osPlatform === 'win32') {
-      return `del "${filePath}"`;
+      return `del ${escapedFilePath}`;
     }
 
     const opts = force ? ['-f'] : [];
-    return `rm ${opts.join(' ')} "${filePath}"`;
+    const escapedOpts = opts.join(' ');
+    return `rm ${escapedOpts} ${escapedFilePath}`.trim();
   }
 
   public removeDir(dir: string): string {
+    const escapedDir = shellEscapeForBundlingCommand(dir, this.osPlatform);
+
     if (this.osPlatform === 'win32') {
-      return `rmdir /s /q "${dir}"`;
+      return `rmdir /s /q ${escapedDir}`;
     }
 
-    return `rm -rf "${dir}"`;
+    return `rm -rf ${escapedDir}`;
   }
 }
 
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
…ate methods

Split getLocalBundlingProvider into:
- executePreBundlingCommands: hooks and tsc pre-compilation (shell)
- executeEsbuild: direct spawnSync without shell
- executePostBundlingCommands: deps installation and hooks (shell)
…ution

Extract executeShellCommands() helper that encapsulates the win32/posix
shell selection, flag, windowsVerbatimArguments, and shared exec options.
Used by both executePreBundlingCommands and executePostBundlingCommands.
@aws-cdk-automation aws-cdk-automation removed the pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes. label Feb 20, 2026
…bundling

Use a single shell session for local bundling (same as Docker) so that
environment variables and shell state propagate across hooks, tsc, esbuild,
and deps installation. Esbuild arguments are shell-escaped via
shellEscapeForBundlingCommand() to neutralize metacharacters while
preserving backward compatibility for hooks.

This removes the need for execDirect(), runBinCommandAsArray(), and the
multi-phase execution model. Both bundling paths now share the same
createBundlingCommand() logic with shell escaping.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contribution/core This is a PR that came from AWS. p2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants