From 999a67ca12ca09172b5a258fa1360f866b681f54 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 19 Aug 2024 10:03:03 +0200 Subject: [PATCH 1/2] feat: add `COREPACK_ON_UNVERIFIED_DOWNLOAD` env variable --- README.md | 7 ++ sources/corepackUtils.ts | 12 +++- tests/Up.test.ts | 4 +- tests/Use.test.ts | 4 +- tests/main.test.ts | 134 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 155 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dd32b4ad0..a92432bed 100644 --- a/README.md +++ b/README.md @@ -349,6 +349,13 @@ same major line. Should you need to upgrade to a new major, use an explicit environment variables are required and as plain text. If you want to send an empty password, explicitly set `COREPACK_NPM_PASSWORD` to an empty string. +- `COREPACK_ON_UNVERIFIED_DOWNLOAD` can be set to: + - `warn` (case insensitive): attempting to download an unsigned version without + providing a hash will emit a warning to stderr. + - `error` (case insensitive): attempting to download an unsigned version without + providing a hash will emit a warning to stderr. + - `ignore` (or any other unsupported value): disables that security feature. + - `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are supported through [`NODE_USE_ENV_PROXY=1`](https://nodejs.org/api/cli.html#node_use_env_proxy1). diff --git a/sources/corepackUtils.ts b/sources/corepackUtils.ts index c02571ab6..8e922473d 100644 --- a/sources/corepackUtils.ts +++ b/sources/corepackUtils.ts @@ -305,7 +305,17 @@ export async function installVersion(installTarget: string, locator: Locator, {s build[1] = Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`); } } - if (build[1] && actualHash !== build[1]) + if (!build[1]) { + const {COREPACK_ON_UNVERIFIED_DOWNLOAD} = process.env; + debugUtils.log(`No hash provided, and signature was not verified; checking COREPACK_ON_UNVERIFIED_DOWNLOAD, set to: ${COREPACK_ON_UNVERIFIED_DOWNLOAD}`); + switch (COREPACK_ON_UNVERIFIED_DOWNLOAD?.toUpperCase()) { + case `ERROR`: + throw new Error(`Integrity of ${locator.name}@${version} could not be verified. Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable. Please provide a hash.`); + + case `WARN`: + console.warn(`Integrity of ${locator.name}@${version} could not be verified. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.`); + } + } else if (actualHash !== build[1]) throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`); const serializedHash = `${algo}.${actualHash}`; diff --git a/tests/Up.test.ts b/tests/Up.test.ts index dac6e9d96..513e203d1 100644 --- a/tests/Up.test.ts +++ b/tests/Up.test.ts @@ -24,9 +24,11 @@ describe(`UpCommand`, () => { packageManager: `yarn@2.1.0`, }); + process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`; + await expect(runCli(cwd, [`up`])).resolves.toMatchObject({ exitCode: 0, - stderr: ``, + stderr: `Integrity of yarn@2.4.3 could not be verified. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.\n`, stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/), }); diff --git a/tests/Use.test.ts b/tests/Use.test.ts index 978c2e3a8..a03e4b22a 100644 --- a/tests/Use.test.ts +++ b/tests/Use.test.ts @@ -136,9 +136,11 @@ describe(`UseCommand`, () => { const subfolder = ppath.join(cwd, `subfolder`); await xfs.mkdirPromise(subfolder); + process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`; + await expect(runCli(subfolder, [`use`, `yarn@2.2.2`])).resolves.toMatchObject({ exitCode: 0, - stderr: ``, + stderr: `Integrity of yarn@2.2.2 could not be verified. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.\n`, }); await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ exitCode: 0, diff --git a/tests/main.test.ts b/tests/main.test.ts index 507db2e43..dd327ebfc 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -855,7 +855,7 @@ it(`should support disabling the network accesses from the environment`, async ( await xfs.mktempPromise(async cwd => { await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { - packageManager: `yarn@2.2.2`, + packageManager: `yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11`, }); await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ @@ -880,12 +880,12 @@ describe(`read-only and offline environment`, () => { // Prepare fake project await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { - packageManager: `yarn@2.2.2`, + packageManager: `yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11`, }); // $ corepack install await expect(runCli(cwd, [`install`])).resolves.toMatchObject({ - stdout: `Adding yarn@2.2.2 to the cache...\n`, + stdout: `Adding yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11 to the cache...\n`, stderr: ``, exitCode: 0, }); @@ -1459,6 +1459,134 @@ describe(`should pick up COREPACK_INTEGRITY_KEYS from env`, () => { }); }); +describe(`unverified downloads`, () => { + beforeEach(() => { + process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`; // See `_registryServer.mjs` + process.env.COREPACK_DEFAULT_TO_LATEST = `1`; + process.env.COREPACK_INTEGRITY_KEYS = `0`; + }); + + it(`from env variable`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {}); + + process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `error`; + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`), + }); + await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`), + }); + + process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `ignore`; + await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `yarn: Hello from custom registry\n`, + stderr: ``, // No warning expected + }); + + process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`; + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`), + }); + await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `yarn: Hello from custom registry\n`, + stderr: ``, // Already cached, no warning expected + }); + }); + }); + + it(`from .corepack.env file`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {}); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`); + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`), + }); + await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`), + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=ignore\n`); + await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `yarn: Hello from custom registry\n`, + stderr: ``, // No warning expected + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=warn\n`); + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`), + }); + await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `yarn: Hello from custom registry\n`, + stderr: ``, // Already cached, no warning expected + }); + }); + }); + + it(`from env file defined by COREPACK_ENV_FILE`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + }); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`); + await xfs.writeFilePromise(ppath.join(cwd, `.other.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=warn\n`); + + // By default, Corepack should be using .corepack.env and fail. + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`), + }); + + process.env.COREPACK_ENV_FILE = `.other.env`; + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`), + }); + }); + }); + + it(`from env even if there's a .corepack.env file`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {}); + + await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`); + + // By default, Corepack should be using .corepack.env (or the built-in ones on Node.js 18.x) and fail. + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 1, + stdout: ``, + stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`), + }); + + process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`; + await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({ + exitCode: 0, + stdout: `pnpm: Hello from custom registry\n`, + stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified`), + }); + }); + }); +}); + for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK_NPM_PASSWORD`, `PROXY`]) { describe(`custom registry with auth ${authType}`, () => { beforeEach(() => { From eeb63943b828813f8a8a291bce6242c4f065eb05 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 2 Jun 2026 19:02:10 +0200 Subject: [PATCH 2/2] fixup! feat: add `COREPACK_ON_UNVERIFIED_DOWNLOAD` env variable lint --- sources/corepackUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sources/corepackUtils.ts b/sources/corepackUtils.ts index 8e922473d..433540bf0 100644 --- a/sources/corepackUtils.ts +++ b/sources/corepackUtils.ts @@ -315,8 +315,9 @@ export async function installVersion(installTarget: string, locator: Locator, {s case `WARN`: console.warn(`Integrity of ${locator.name}@${version} could not be verified. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.`); } - } else if (actualHash !== build[1]) + } else if (actualHash !== build[1]) { throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`); + } const serializedHash = `${algo}.${actualHash}`;