Skip to content

Commit 3ad862d

Browse files
committed
fix(dev): verify sha-256 checksums for downloaded cloudflared binaries
1 parent dd22149 commit 3ad862d

2 files changed

Lines changed: 30 additions & 4 deletions

File tree

packages/nuxt-cli/src/dev/binaries.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Buffer } from 'node:buffer'
22
import { execFileSync } from 'node:child_process'
3+
import { createHash } from 'node:crypto'
34
import { chmodSync, existsSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs'
45

56
import process from 'node:process'
@@ -33,13 +34,13 @@ interface ConsentOptions {
3334
* `cacheName` overrides the cached filename, so version-pinned tools can key
3435
* their cache entry by version without affecting the `PATH` lookup.
3536
*/
36-
export function resolveTool(name: string, options: { url?: string, archive?: boolean, cacheName?: string, consent: ConsentOptions }): Promise<string | undefined> {
37+
export function resolveTool(name: string, options: { url?: string, archive?: boolean, cacheName?: string, sha256?: string, consent: ConsentOptions }): Promise<string | undefined> {
3738
// The consent prompt and the download indicator both redraw by moving the
3839
// cursor, so they need stdout back from consola for the duration.
3940
return withDirectStdout(() => locateTool(name, options))
4041
}
4142

42-
async function locateTool(name: string, options: { url?: string, archive?: boolean, cacheName?: string, consent: ConsentOptions }): Promise<string | undefined> {
43+
async function locateTool(name: string, options: { url?: string, archive?: boolean, cacheName?: string, sha256?: string, consent: ConsentOptions }): Promise<string | undefined> {
4344
const existing = findInPath(name)
4445
if (existing) {
4546
return existing
@@ -64,7 +65,7 @@ async function locateTool(name: string, options: { url?: string, archive?: boole
6465
if (!await confirmToolInstall(options.consent)) {
6566
return undefined
6667
}
67-
return downloadBinary(url, destination, { archive: options.archive, name })
68+
return downloadBinary(url, destination, { archive: options.archive, name, sha256: options.sha256 })
6869
})
6970
}
7071

@@ -105,7 +106,7 @@ async function confirmToolInstall(options: ConsentOptions): Promise<boolean> {
105106

106107
const RESPONSE_TIMEOUT_MS = 30_000
107108

108-
async function downloadBinary(url: string, destination: string, options: { archive?: boolean, name?: string } = {}): Promise<string | undefined> {
109+
async function downloadBinary(url: string, destination: string, options: { archive?: boolean, name?: string, sha256?: string } = {}): Promise<string | undefined> {
109110
const label = options.name || url
110111
try {
111112
// The deadline covers connecting and headers only; once bytes are flowing the
@@ -123,6 +124,12 @@ async function downloadBinary(url: string, destination: string, options: { archi
123124
throw new Error(`Unexpected response: ${response.status}`)
124125
}
125126
const data = await readWithProgress(response, label)
127+
if (options.sha256) {
128+
const digest = createHash('sha256').update(data).digest('hex')
129+
if (digest !== options.sha256.toLowerCase()) {
130+
throw new Error(`SHA-256 mismatch for \`${label}\`: expected ${options.sha256}, got ${digest}. The download was discarded.`)
131+
}
132+
}
126133
// Stage the install and rename into place: two dev servers racing to install
127134
// the same tool would otherwise interleave writes and cache a corrupt binary.
128135
// Staged inside the cache directory so the rename stays on one filesystem.

packages/nuxt-cli/src/dev/tunnel.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,24 @@ const CLOUDFLARED_ASSETS: Record<string, Partial<Record<typeof process.arch, str
161161
},
162162
}
163163

164+
/**
165+
* SHA-256 checksums of the {@link DEFAULT_CLOUDFLARED_VERSION} release assets,
166+
* from the release notes at
167+
* https://github.com/cloudflare/cloudflared/releases/tag/2026.7.3.
168+
* Must be updated together with the pinned version. Downloads of an overridden
169+
* `CLOUDFLARED_VERSION` cannot be verified and are accepted as fetched.
170+
*/
171+
const CLOUDFLARED_SHA256: Record<string, string> = {
172+
'cloudflared-darwin-arm64.tgz': 'f35c50089cd25f77a4cb5a2152036bc26db15aa31fbe11f7995d2e42a4ed6257',
173+
'cloudflared-darwin-amd64.tgz': 'e88fe5874d42a94f49a7ea59cabc3722d2962d0449232b0f3b1a426a712e275c',
174+
'cloudflared-linux-arm': '6dadd979b8833760e9f6d840a6239a8c08c8bcf73b4231ec537f483873f37c73',
175+
'cloudflared-linux-arm64': '65259e652a7bea08bf5df603233ab22b8bf3116af8df9f9206209af6a1b955c0',
176+
'cloudflared-linux-386': '6c982e77e644644f5bce76781dd2b69ddc0bfa5e1dd1f55f0037850ac0946771',
177+
'cloudflared-linux-amd64': '9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17',
178+
'cloudflared-windows-386.exe': 'd026e39d9be21c70ea652528fda2801e164d5e25688b7b0fb3b65080cbd96503',
179+
'cloudflared-windows-amd64.exe': '8635da433b6df8194746e88ed9d2589566c20e38bfc2a80e431a348b7c765841',
180+
}
181+
164182
async function resolveCloudflared(): Promise<string | undefined> {
165183
const CLOUDFLARED_VERSION = resolveCloudflaredVersion()
166184
const base = CLOUDFLARED_VERSION === 'latest'
@@ -171,6 +189,7 @@ async function resolveCloudflared(): Promise<string | undefined> {
171189
url: asset && `${base}/${asset}`,
172190
archive: asset?.endsWith('.tgz'),
173191
cacheName: `cloudflared-${CLOUDFLARED_VERSION}`,
192+
sha256: asset && CLOUDFLARED_VERSION === DEFAULT_CLOUDFLARED_VERSION ? CLOUDFLARED_SHA256[asset] : undefined,
174193
consent: {
175194
key: 'cloudflared',
176195
notice: [

0 commit comments

Comments
 (0)