Skip to content

Commit 9ff053e

Browse files
committed
security: add integrity checks to the Deno API
1 parent 0a9bf21 commit 9ff053e

4 files changed

Lines changed: 22 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88

99
Thanks to [@dellalibera](https://github.com/dellalibera) for reporting this issue.
1010

11+
* Add integrity checks to the Deno API ([GHSA-gv7w-rqvm-qjhr](https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr))
12+
13+
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.
14+
15+
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.
16+
17+
Thanks to [@sondt99](https://github.com/sondt99) for reporting this issue.
18+
1119
* Avoid inlining `using` and `await using` declarations ([#4482](https://github.com/evanw/esbuild/issues/4482))
1220

1321
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:

lib/deno/mod.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ourselves from "./mod"
44
import * as denoflate from "https://deno.land/x/denoflate@1.2.1/mod.ts"
55

66
declare const ESBUILD_VERSION: string
7+
declare const ESBUILD_BINARY_HASHES: Record<string, string>
78

89
export let version = ESBUILD_VERSION
910

@@ -59,6 +60,15 @@ export const initialize: typeof types.initialize = async (options) => {
5960
initializeWasCalled = true
6061
}
6162

63+
async function binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array): Promise<void> {
64+
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes as Uint8Array<ArrayBuffer>)
65+
const hash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('')
66+
const key = `${pkg}/${subpath}`
67+
const expected = ESBUILD_BINARY_HASHES[key]
68+
if (!expected) throw new Error(`Missing hash for "${key}"`)
69+
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..." for "${pkg}"`)
70+
}
71+
6272
async function installFromNPM(name: string, subpath: string): Promise<string> {
6373
const { finalPath, finalDir } = getCachePath(name)
6474
try {
@@ -71,6 +81,7 @@ async function installFromNPM(name: string, subpath: string): Promise<string> {
7181
const url = `${npmRegistry}/${name}/-/${name.replace("@esbuild/", "")}-${version}.tgz`
7282
const buffer = await fetch(url).then(r => r.arrayBuffer())
7383
const executable = extractFileFromTarGzip(new Uint8Array(buffer), subpath)
84+
await binaryIntegrityCheck(name, subpath, executable)
7485

7586
await Deno.mkdir(finalDir, {
7687
recursive: true,

lib/npm/node-install.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function binaryIntegrityCheck(pkg: string, subpath: string, bytes: Uint8Array):
230230
const key = `${pkg}/${subpath}`
231231
const expected = packageJSON['esbuild.binaryHashes'][key]
232232
if (!expected) throw new Error(`Missing hash for "${key}"`)
233-
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..."`)
233+
if (hash !== expected) throw new Error(`"${hash.slice(0, 8)}..." doesn't match "${expected.slice(0, 8)}..." for "${pkg}"`)
234234
}
235235

236236
async function downloadDirectlyFromNPM(pkg: string, subpath: string, binPath: string): Promise<void> {

scripts/esbuild.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,14 @@ exports.buildWasmLib = async (esbuildPath) => {
273273

274274
const buildDenoLib = async (esbuildPath) => {
275275
// Generate "deno/mod.js"
276+
const packageJSON = JSON.parse(fs.readFileSync(path.join(npmDir, 'package.json'), 'utf8'))
276277
childProcess.execFileSync(esbuildPath, [
277278
path.join(repoDir, 'lib', 'deno', 'mod.ts'),
278279
'--bundle',
279280
'--outfile=' + path.join(denoDir, 'mod.js'),
280281
'--target=' + denoTarget,
281282
'--define:ESBUILD_VERSION=' + JSON.stringify(version),
283+
'--define:ESBUILD_BINARY_HASHES=' + JSON.stringify(packageJSON['esbuild.binaryHashes'], null, 2),
282284
'--platform=neutral',
283285
'--log-level=warning',
284286
'--banner:js=/// <reference types="./mod.d.ts" />',

0 commit comments

Comments
 (0)