security: add integrity checks to the Deno API

This commit is contained in:
Evan Wallace
2026-06-11 18:09:32 -04:00
parent 0a9bf2135b
commit 9ff053e53b
4 changed files with 22 additions and 1 deletions
+8
View File
@@ -8,6 +8,14 @@
Thanks to [@dellalibera](https://github.com/dellalibera) for reporting this issue.
* Add integrity checks to the Deno API ([GHSA-gv7w-rqvm-qjhr](https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr))
The previous release of esbuild added integrity checks to esbuild's npm install script. This release also adds integrity checks to esbuild's Deno install script. Now esbuild's Deno API will also fail with an error if the downloaded esbuild binary contains something other than the expected content.
Note that esbuild's Deno API installs from `registry.npmjs.org` by default, but allows the `NPM_CONFIG_REGISTRY` environment variable to override this with a custom package registry. This change means that the esbuild executable served by `NPM_CONFIG_REGISTRY` must now match the expected content.
Thanks to [@sondt99](https://github.com/sondt99) for reporting this issue.
* Avoid inlining `using` and `await using` declarations ([#4482](https://github.com/evanw/esbuild/issues/4482))
Previously esbuild's minifier sometimes incorrectly inlined `using` and `await using` declarations into subsequent uses of that declaration, which then fails to dispose of the resource correctly. This bug happened because inlining was done for `let` and `const` declarations by avoiding doing it for `var` declarations, which no longer worked when more declaration types were added. Here's an example:
+11
View File
@@ -4,6 +4,7 @@ import * as ourselves from "./mod"
import * as denoflate from "https://deno.land/x/[email protected]/mod.ts"
declare const ESBUILD_VERSION: string
declare const ESBUILD_BINARY_HASHES: Record<string, string>
export let version = ESBUILD_VERSION
@@ -59,6 +60,15 @@ export const initialize: typeof types.initialize = async (options) => {
initializeWasCalled = true
}
async function binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array): Promise<void> {
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes as Uint8Array<ArrayBuffer>)
const hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
const key = `${pkg}/${subpath}`
const expected = ESBUILD_BINARY_HASHES[key]
if (!expected) throw new Error(`Missing hash for "${key}"`)
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..." for "${pkg}"`)
}
async function installFromNPM(name: string, subpath: string): Promise<string> {
const { finalPath, finalDir } = getCachePath(name)
try {
@@ -71,6 +81,7 @@ async function installFromNPM(name: string, subpath: string): Promise<string> {
const url = `${npmRegistry}/${name}/-/${name.replace("@esbuild/", "")}-${version}.tgz`
const buffer = await fetch(url).then(r => r.arrayBuffer())
const executable = extractFileFromTarGzip(new Uint8Array(buffer), subpath)
await binaryIntegrityCheck(name, subpath, executable)
await Deno.mkdir(finalDir, {
recursive: true,
+1 -1
View File
@@ -230,7 +230,7 @@ function binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array):
const key = `${pkg}/${subpath}`
const expected = packageJSON['esbuild.binaryHashes'][key]
if (!expected) throw new Error(`Missing hash for "${key}"`)
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..."`)
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..." for "${pkg}"`)
}
async function downloadDirectlyFromNPM(pkg: string, subpath: string, binPath: string): Promise<void> {
+2
View File
@@ -273,12 +273,14 @@ exports.buildWasmLib = async (esbuildPath) => {
const buildDenoLib = async (esbuildPath) => {
// Generate "deno/mod.js"
const packageJSON = JSON.parse(fs.readFileSync(path.join(npmDir, 'package.json'), 'utf8'))
childProcess.execFileSync(esbuildPath, [
path.join(repoDir, 'lib', 'deno', 'mod.ts'),
'--bundle',
'--outfile=' + path.join(denoDir, 'mod.js'),
'--target=' + denoTarget,
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
'--define:ESBUILD_BINARY_HASHES=' + JSON.stringify(packageJSON['esbuild.binaryHashes'], null, 2),
'--platform=neutral',
'--log-level=warning',
'--banner:js=/// <reference types="./mod.d.ts" />',