OS Command Injection in `wrangler pages deploy`
Description
SummaryA command injection vulnerability (CWE-78) has been found to exist in the wrangler pages deploy command. The issue occurs because the --commit-hash parameter is passed directly to a shell command without proper validation or sanitization, allowing an attacker with control of --commit-hash to execute arbitrary commands on the system running Wrangler.
Root causeThe commitHash variable, derived from user input via the --commit-hash CLI argument, is interpolated directly into a shell command using template literals (e.g., execSync(git show -s --format=%B ${commitHash})). Shell metacharacters are interpreted by the shell, enabling command execution.
ImpactThis vulnerability is generally hard to exploit, as it requires --commit-hash to be attacker controlled. The vulnerability primarily affects CI/CD environments where wrangler pages deploy is used in automated pipelines and the
--commit-hash parameter is populated from external, potentially untrusted sources. An attacker could exploit this to:
- Run any shell command.
- Exfiltrate environment variables.
- Compromise the CI runner to install backdoors or modify build artifacts.
Credits Disclosed responsibly by kny4hacker.
Mitigation * Wrangler v4 users are requested to upgrade to Wrangler v4.59.1 or higher. * Wrangler v3 users are requested to upgrade to Wrangler v3.114.17 or higher. * Users on Wrangler v2 (EOL) should upgrade to a supported major version.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
wranglernpm | >= 2.0.15, < 3.114.17 | 3.114.17 |
wranglernpm | >= 4.0.0, < 4.59.1 | 4.59.1 |
Affected products
1- Range: @cloudflare/chrome-devtools-patches@0.1.1, @cloudflare/chrome-devtools-patches@0.1.2, @cloudflare/chrome-devtools-patches@0.1.3, …
Patches
199b1f328a9affix: execute git commands in pages deploy safely (#11889)
3 files changed · +126 −2
.changeset/fix-pages-deploy-command-injection.md+7 −0 added@@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Use argument array when executing git commands with `wrangler pages deploy` + +Pass user provided values from `--commit-hash` safely to underlying git command.
packages/wrangler/src/pages/deploy.ts+7 −2 modified@@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { execFileSync, execSync } from "node:child_process"; import { writeFile } from "node:fs/promises"; import path from "node:path"; import { @@ -361,7 +361,12 @@ export const pagesDeployCommand = createCommand({ } if (!commitMessage) { - commitMessage = execSync(`git show -s --format=%B ${commitHash}`) + commitMessage = execFileSync("git", [ + "show", + "-s", + "--format=%B", + commitHash, + ]) .toString() .trim(); }
packages/wrangler/src/__tests__/pages/deploy.test.ts+112 −0 modified@@ -5714,6 +5714,118 @@ and that at least one include rule is provided. }); }); + describe("deploys with custom commit information", () => { + it("should accept and send --commit-hash parameter", async () => { + mkdirSync("public"); + writeFileSync("public/README.md", "# Test project"); + + mockGetUploadTokenRequest( + "<<funfetti-auth-jwt>>", + "some-account-id", + "foo" + ); + + let deploymentFormData: Record<string, unknown> | null = null; + + msw.use( + http.post( + "*/pages/assets/check-missing", + async () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: [], + }, + { status: 200 } + ); + }, + { once: true } + ), + http.post("*/pages/assets/upload", async () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: null, + }, + { status: 200 } + ); + }), + http.get("*/accounts/:accountId/pages/projects/foo", async () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { deployment_configs: { production: {}, preview: {} } }, + }, + { status: 200 } + ); + }), + http.post( + "*/accounts/:accountId/pages/projects/foo/deployments", + async ({ request }) => { + const formData = await request.formData(); + const formDataObj: Record<string, unknown> = {}; + for (const [key, value] of formData.entries()) { + formDataObj[key] = value; + } + deploymentFormData = formDataObj; + + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { + id: "123-456-789", + url: "https://abcxyz.foo.pages.dev/", + }, + }, + { status: 200 } + ); + }, + { once: true } + ), + http.get( + "*/accounts/:accountId/pages/projects/foo/deployments/:deploymentId", + async () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: { + id: "123-456-789", + latest_stage: { + name: "deploy", + status: "success", + }, + }, + }, + { status: 200 } + ); + } + ) + ); + + await runWrangler( + "pages deploy public --project-name=foo --commit-hash=abc123def456 --commit-message='Test commit'" + ); + + // Verify the commit_hash was sent in the deployment request + expect(deploymentFormData).not.toBeNull(); + expect(deploymentFormData).toHaveProperty("commit_hash", "abc123def456"); + expect(deploymentFormData).toHaveProperty( + "commit_message", + "Test commit" + ); + }); + }); + describe("deploys using redirected configs", () => { let fooProjectDetailsChecked = false;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-36p8-mvp6-cv38ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-0933ghsaADVISORY
- github.com/cloudflare/workers-sdk/commit/99b1f328a9afe181b49f1114ed47f15f6d25f0beghsaWEB
- github.com/cloudflare/workers-sdk/releases/tag/wrangler%403.114.17ghsaWEB
- github.com/cloudflare/workers-sdk/releases/tag/wrangler%404.59.1ghsaWEB
- github.com/cloudflare/workers-sdk/security/advisories/GHSA-36p8-mvp6-cv38ghsaWEB
News mentions
0No linked articles in our index yet.