-
Notifications
You must be signed in to change notification settings - Fork 145
fix: harden runWrangler against shell injection and silence DEP0190 #1248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
726e3ef
5df6c61
227371e
9f39728
382e5d1
229c45f
5a5a768
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,21 @@ | ||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
| "@opennextjs/cloudflare": patch | ||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| fix: harden `runWrangler` against shell injection and silence Node.js DEP0190. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| `runWrangler` previously called `spawnSync` with `shell: true` while interpolating | ||||||||||||||||||||||||||||||||||||||
| flag values into single-string array entries (e.g. `` `--env ${wranglerOpts.environment}` ``). | ||||||||||||||||||||||||||||||||||||||
| With `shell: true`, Node.js joins the arguments into a shell command string without | ||||||||||||||||||||||||||||||||||||||
| escaping, so a value containing shell metacharacters (`;`, `&&`, `$(...)`, backticks, | ||||||||||||||||||||||||||||||||||||||
| etc.) could break out of its intended argument boundary. Node.js 22 added `DEP0190` | ||||||||||||||||||||||||||||||||||||||
| specifically to flag this pattern, and that warning surfaced on every `opennextjs-cloudflare deploy` | ||||||||||||||||||||||||||||||||||||||
| invoked through `wrangler-action` on Node 22+. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Each flag/value pair is now passed as separate array entries, and `shell` is set to | ||||||||||||||||||||||||||||||||||||||
| `false` on POSIX (where the package manager binary can be invoked directly). `shell: true` | ||||||||||||||||||||||||||||||||||||||
| is retained on Windows so the package manager's `.cmd` shim still resolves via `cmd.exe`, | ||||||||||||||||||||||||||||||||||||||
| but values are no longer interpolated into the shell command, so the injection vector | ||||||||||||||||||||||||||||||||||||||
| is closed on every platform. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Closes [#1182](https://github.com/opennextjs/opennextjs-cloudflare/issues/1182). | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is used for the changelog. As long as the changes are described in the PR, we should be good. Thanks!
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import type yargs from "yargs"; | |
| import { DEPLOYMENT_MAPPING_ENV_NAME } from "../templates/skew-protection.js"; | ||
| import { populateCache, withPopulateCacheOptions } from "./populate-cache.js"; | ||
| import { getDeploymentMapping } from "./skew-protection.js"; | ||
| import { getEnvFromPlatformProxy, quoteShellMeta } from "./utils/helpers.js"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is |
||
| import { getEnvFromPlatformProxy } from "./utils/helpers.js"; | ||
| import { runWrangler } from "./utils/run-wrangler.js"; | ||
| import type { WithWranglerArgs } from "./utils/utils.js"; | ||
| import { | ||
|
|
@@ -58,7 +58,7 @@ export async function deployCommand(args: WithWranglerArgs<{ cacheChunkSize?: nu | |
| "deploy", | ||
| ...args.wranglerArgs, | ||
| ...(deploymentMapping | ||
| ? [`--var ${DEPLOYMENT_MAPPING_ENV_NAME}:${quoteShellMeta(JSON.stringify(deploymentMapping))}`] | ||
| ? ["--var", `${DEPLOYMENT_MAPPING_ENV_NAME}:${JSON.stringify(deploymentMapping)}`] | ||
| : []), | ||
| ], | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,7 +43,7 @@ import { | |||||||
| import { ensureR2Bucket } from "../utils/ensure-r2-bucket.js"; | ||||||||
| import { normalizePath } from "../utils/normalize-path.js"; | ||||||||
| import type { R2Response } from "../workers/r2-cache-types.js"; | ||||||||
| import { getEnvFromPlatformProxy, quoteShellMeta, type WorkerEnvVar } from "./utils/helpers.js"; | ||||||||
| import { getEnvFromPlatformProxy, type WorkerEnvVar } from "./utils/helpers.js"; | ||||||||
| import type { WranglerTarget } from "./utils/run-wrangler.js"; | ||||||||
| import { runWrangler } from "./utils/run-wrangler.js"; | ||||||||
| import type { WithWranglerArgs } from "./utils/utils.js"; | ||||||||
|
|
@@ -530,10 +530,14 @@ async function populateKVIncrementalCache( | |||||||
| const result = runWrangler( | ||||||||
| buildOpts, | ||||||||
| [ | ||||||||
| "kv bulk put", | ||||||||
| quoteShellMeta(chunkPath), | ||||||||
| `--binding ${KV_CACHE_BINDING_NAME}`, | ||||||||
| `--preview ${populateCacheOptions.shouldUsePreviewId}`, | ||||||||
| "kv", | ||||||||
| "bulk", | ||||||||
| "put", | ||||||||
| chunkPath, | ||||||||
| "--binding", | ||||||||
| KV_CACHE_BINDING_NAME, | ||||||||
| "--preview", | ||||||||
| String(populateCacheOptions.shouldUsePreviewId), | ||||||||
| ], | ||||||||
| { | ||||||||
| target: populateCacheOptions.target, | ||||||||
|
|
@@ -570,15 +574,18 @@ function populateD1TagCache( | |||||||
| const result = runWrangler( | ||||||||
| buildOpts, | ||||||||
| [ | ||||||||
| "d1 execute", | ||||||||
| "d1", | ||||||||
| "execute", | ||||||||
| D1_TAG_BINDING_NAME, | ||||||||
| // Columns: | ||||||||
| // tag - The cache tag. | ||||||||
| // revalidatedAt - Timestamp (ms) when the tag was last revalidated. | ||||||||
| // stale - Timestamp (ms) when the cached entry becomes stale. Added in v1.19. | ||||||||
| // expire - Timestamp (ms) when the cached entry expires. NULL means no expire. Added in v1.19. | ||||||||
| `--command "CREATE TABLE IF NOT EXISTS revalidations (tag TEXT NOT NULL, revalidatedAt INTEGER NOT NULL, stale INTEGER, expire INTEGER default NULL, UNIQUE(tag) ON CONFLICT REPLACE);"`, | ||||||||
| `--preview ${populateCacheOptions.shouldUsePreviewId}`, | ||||||||
| "--command", | ||||||||
| "CREATE TABLE IF NOT EXISTS revalidations (tag TEXT NOT NULL, revalidatedAt INTEGER NOT NULL, stale INTEGER, expire INTEGER default NULL, UNIQUE(tag) ON CONFLICT REPLACE);", | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think the sql statements might still need to be quoted for windows? |
||||||||
| "--preview", | ||||||||
| String(populateCacheOptions.shouldUsePreviewId), | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about:
Suggested change
|
||||||||
| ], | ||||||||
| { | ||||||||
| target: populateCacheOptions.target, | ||||||||
|
|
@@ -598,10 +605,13 @@ function populateD1TagCache( | |||||||
| runWrangler( | ||||||||
| buildOpts, | ||||||||
| [ | ||||||||
| "d1 execute", | ||||||||
| "d1", | ||||||||
| "execute", | ||||||||
| D1_TAG_BINDING_NAME, | ||||||||
| `--command "ALTER TABLE revalidations ADD COLUMN stale INTEGER; ALTER TABLE revalidations ADD COLUMN expire INTEGER default NULL"`, | ||||||||
| `--preview ${populateCacheOptions.shouldUsePreviewId}`, | ||||||||
| "--command", | ||||||||
| "ALTER TABLE revalidations ADD COLUMN stale INTEGER; ALTER TABLE revalidations ADD COLUMN expire INTEGER default NULL", | ||||||||
| "--preview", | ||||||||
| String(populateCacheOptions.shouldUsePreviewId), | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||||||||
| ], | ||||||||
| { | ||||||||
| target: populateCacheOptions.target, | ||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,19 +102,22 @@ export function runWrangler( | |
| [ | ||
| options.packager === "bun" ? "x" : "exec", | ||
| "wrangler", | ||
| ...injectPassthroughFlagForArgs( | ||
| options, | ||
| [ | ||
| ...args, | ||
| wranglerOpts.environment && `--env ${wranglerOpts.environment}`, | ||
| wranglerOpts.configPath && `--config ${wranglerOpts.configPath}`, | ||
| wranglerOpts.target === "remote" && "--remote", | ||
| wranglerOpts.target === "local" && "--local", | ||
| ].filter((v): v is string => !!v) | ||
| ), | ||
| ...injectPassthroughFlagForArgs(options, [ | ||
| ...args, | ||
| ...(wranglerOpts.environment ? ["--env", wranglerOpts.environment] : []), | ||
| ...(wranglerOpts.configPath ? ["--config", wranglerOpts.configPath] : []), | ||
| ...(wranglerOpts.target === "remote" ? ["--remote"] : []), | ||
| ...(wranglerOpts.target === "local" ? ["--local"] : []), | ||
| ]), | ||
| ], | ||
| { | ||
| shell: true, | ||
| // Each flag value is now a separate array entry, so `shell: true` is no longer | ||
| // needed for value interpolation and would only re-introduce the injection vector | ||
| // flagged by Node.js DEP0190 (and the related security advisory). `shell: true` is | ||
| // still required on Windows so that the package manager's `.cmd` shim is resolved | ||
| // via cmd.exe; with each arg as a separate entry, Node escapes them correctly even | ||
| // when the shell is involved. | ||
| shell: process.platform === "win32", | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we still use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh now I understand this would trigger DEP0190. Could we please reword the comment to something like: Setting Note that |
||
| // Always pipe stderr so that we can capture it for inspection. | ||
| // Keep stdin and stdout as "inherit" when not piping logs to maintain TTY detection | ||
| // (wrangler checks `process.stdin.isTTY && process.stdout.isTTY` for interactive mode). | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't it still left open on windows?