Skip to content

Commit 0125d89

Browse files
authored
feat: add COREPACK_ON_UNVERIFIED_DOWNLOAD env variable (#856)
1 parent b26c9d5 commit 0125d89

5 files changed

Lines changed: 251 additions & 21 deletions

File tree

‎README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,19 @@ same major line. Should you need to upgrade to a new major, use an explicit
349349
environment variables are required and as plain text. If you want to send an
350350
empty password, explicitly set `COREPACK_NPM_PASSWORD` to an empty string.
351351

352+
- `COREPACK_ON_UNVERIFIED_DOWNLOAD` can be set to:
353+
- `warn` (case insensitive): attempting to download an unsigned version without
354+
providing a hash will emit a warning to stderr.
355+
- `error` (case insensitive): attempting to download an unsigned version without
356+
providing a hash will fail with an error, and nothing gets downloaded.
357+
- `strict-warn` (case insensitive): same as `warn`, and additionally emits a
358+
warning when downloading a version that is not pinned by a hash, even when
359+
its signature can be verified.
360+
- `strict-error` (case insensitive): same as `error`, and additionally fails
361+
when downloading a version that is not pinned by a hash, even when its
362+
signature can be verified.
363+
- `ignore` (or any other unsupported value): disables that security feature.
364+
352365
- `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are supported through
353366
[`NODE_USE_ENV_PROXY=1`](https://nodejs.org/api/cli.html#node_use_env_proxy1).
354367

‎sources/corepackUtils.ts‎

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -231,14 +231,38 @@ export async function installVersion(installTarget: string, locator: Locator, {s
231231
}
232232
}
233233

234+
const registry = getRegistryFromPackageManagerSpec(spec);
235+
const canVerifySignature = registry.type === `npm` && !registry.bin && !shouldSkipIntegrityCheck();
236+
if (!build[1]) {
237+
const {COREPACK_ON_UNVERIFIED_DOWNLOAD} = process.env;
238+
debugUtils.log(`No hash provided${canVerifySignature ? `` : `, and signature cannot be verified`}; checking COREPACK_ON_UNVERIFIED_DOWNLOAD, set to: ${COREPACK_ON_UNVERIFIED_DOWNLOAD}`);
239+
const mode = COREPACK_ON_UNVERIFIED_DOWNLOAD?.toUpperCase();
240+
// In strict mode, a hash is required even when the signature can be verified.
241+
const isStrict = mode === `STRICT-ERROR` || mode === `STRICT-WARN`;
242+
if (isStrict || !canVerifySignature) {
243+
const reason = canVerifySignature
244+
? `is not pinned by a hash`
245+
: `could not be verified`;
246+
247+
switch (mode) {
248+
case `ERROR`:
249+
case `STRICT-ERROR`:
250+
throw new Error(`Integrity of ${locator.name}@${version} ${reason}. Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable. Please provide a hash.`);
251+
252+
case `WARN`:
253+
case `STRICT-WARN`:
254+
console.warn(`Integrity of ${locator.name}@${version} ${reason}. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.`);
255+
}
256+
}
257+
}
258+
234259
let url: string;
235260
let signatures: Array<{keyid: string, sig: string}>;
236261
let integrity: string;
237262
let binPath: string | null = null;
238263
if (locatorIsASupportedPackageManager) {
239264
url = spec.url.replace(`{}`, version);
240265
if (process.env.COREPACK_NPM_REGISTRY) {
241-
const registry = getRegistryFromPackageManagerSpec(spec);
242266
if (registry.type === `npm`) {
243267
({tarball: url, signatures, integrity} = await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version));
244268
if (registry.bin) {
@@ -294,19 +318,18 @@ export async function installVersion(installTarget: string, locator: Locator, {s
294318
}
295319
}
296320

297-
if (!build[1]) {
298-
const registry = getRegistryFromPackageManagerSpec(spec);
299-
if (registry.type === `npm` && !registry.bin && !shouldSkipIntegrityCheck()) {
300-
if (signatures! == null || integrity! == null)
301-
({signatures, integrity} = (await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version)));
302-
303-
await npmRegistryUtils.verifySignature({signatures, integrity, packageName: registry.package, version});
304-
// @ts-expect-error ignore readonly
305-
build[1] = Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`);
306-
}
321+
if (!build[1] && canVerifySignature && registry.type === `npm`) {
322+
if (signatures! == null || integrity! == null)
323+
({signatures, integrity} = (await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version)));
324+
325+
npmRegistryUtils.verifySignature({signatures, integrity, packageName: registry.package, version});
326+
// @ts-expect-error ignore readonly
327+
build[1] = Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`);
307328
}
308-
if (build[1] && actualHash !== build[1])
329+
if (build[1] && actualHash !== build[1]) {
330+
await fs.promises.rm(tmpFolder, {recursive: true, force: true});
309331
throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`);
332+
}
310333

311334
const serializedHash = `${algo}.${actualHash}`;
312335

‎tests/Up.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ describe(`UpCommand`, () => {
2424
packageManager: `yarn@2.1.0`,
2525
});
2626

27+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
28+
2729
await expect(runCli(cwd, [`up`])).resolves.toMatchObject({
2830
exitCode: 0,
29-
stderr: ``,
31+
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`,
3032
stdout: expect.stringMatching(/^Installing yarn@2\.4\.3 in the project\.\.\.\n\n/),
3133
});
3234

‎tests/Use.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,11 @@ describe(`UseCommand`, () => {
136136
const subfolder = ppath.join(cwd, `subfolder`);
137137
await xfs.mkdirPromise(subfolder);
138138

139+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
140+
139141
await expect(runCli(subfolder, [`use`, `yarn@2.2.2`])).resolves.toMatchObject({
140142
exitCode: 0,
141-
stderr: ``,
143+
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`,
142144
});
143145
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
144146
exitCode: 0,

‎tests/main.test.ts‎

Lines changed: 197 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,7 @@ it(`should support disabling the network accesses from the environment`, async (
856856

857857
await xfs.mktempPromise(async cwd => {
858858
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
859-
packageManager: `yarn@2.2.2`,
859+
packageManager: `yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11`,
860860
});
861861

862862
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
@@ -881,12 +881,12 @@ describe(`read-only and offline environment`, () => {
881881

882882
// Prepare fake project
883883
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
884-
packageManager: `yarn@2.2.2`,
884+
packageManager: `yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11`,
885885
});
886886

887887
// $ corepack install
888888
await expect(runCli(cwd, [`install`])).resolves.toMatchObject({
889-
stdout: `Adding yarn@2.2.2 to the cache...\n`,
889+
stdout: `Adding yarn@2.2.2+sha1.9aede2626b101719cbc1314d61def0742852ba11 to the cache...\n`,
890890
stderr: ``,
891891
exitCode: 0,
892892
});
@@ -1549,6 +1549,196 @@ describe(`should pick up COREPACK_INTEGRITY_KEYS from env`, () => {
15491549
});
15501550
});
15511551

1552+
describe(`unverified downloads`, () => {
1553+
beforeEach(() => {
1554+
process.env.AUTH_TYPE = `COREPACK_NPM_TOKEN`; // See `_registryServer.mjs`
1555+
process.env.COREPACK_DEFAULT_TO_LATEST = `1`;
1556+
process.env.COREPACK_INTEGRITY_KEYS = `0`;
1557+
});
1558+
1559+
it(`from env variable`, async () => {
1560+
await xfs.mktempPromise(async cwd => {
1561+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});
1562+
1563+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `error`;
1564+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1565+
exitCode: 1,
1566+
stdout: ``,
1567+
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
1568+
});
1569+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
1570+
exitCode: 1,
1571+
stdout: ``,
1572+
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
1573+
});
1574+
1575+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `ignore`;
1576+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
1577+
exitCode: 0,
1578+
stdout: `yarn: Hello from custom registry\n`,
1579+
stderr: ``, // No warning expected
1580+
});
1581+
1582+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
1583+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1584+
exitCode: 0,
1585+
stdout: `pnpm: Hello from custom registry\n`,
1586+
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`),
1587+
});
1588+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
1589+
exitCode: 0,
1590+
stdout: `yarn: Hello from custom registry\n`,
1591+
stderr: ``, // Already cached, no warning expected
1592+
});
1593+
});
1594+
});
1595+
1596+
it(`from .corepack.env file`, async () => {
1597+
await xfs.mktempPromise(async cwd => {
1598+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});
1599+
1600+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`);
1601+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1602+
exitCode: 1,
1603+
stdout: ``,
1604+
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
1605+
});
1606+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
1607+
exitCode: 1,
1608+
stdout: ``,
1609+
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
1610+
});
1611+
1612+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=ignore\n`);
1613+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
1614+
exitCode: 0,
1615+
stdout: `yarn: Hello from custom registry\n`,
1616+
stderr: ``, // No warning expected
1617+
});
1618+
1619+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=warn\n`);
1620+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1621+
exitCode: 0,
1622+
stdout: `pnpm: Hello from custom registry\n`,
1623+
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`),
1624+
});
1625+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
1626+
exitCode: 0,
1627+
stdout: `yarn: Hello from custom registry\n`,
1628+
stderr: ``, // Already cached, no warning expected
1629+
});
1630+
});
1631+
});
1632+
1633+
it(`from env file defined by COREPACK_ENV_FILE`, async () => {
1634+
await xfs.mktempPromise(async cwd => {
1635+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1636+
});
1637+
1638+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`);
1639+
await xfs.writeFilePromise(ppath.join(cwd, `.other.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=warn\n`);
1640+
1641+
// By default, Corepack should be using .corepack.env and fail.
1642+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1643+
exitCode: 1,
1644+
stdout: ``,
1645+
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
1646+
});
1647+
1648+
process.env.COREPACK_ENV_FILE = `.other.env`;
1649+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1650+
exitCode: 0,
1651+
stdout: `pnpm: Hello from custom registry\n`,
1652+
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified.`),
1653+
});
1654+
});
1655+
});
1656+
1657+
it(`from env even if there's a .corepack.env file`, async () => {
1658+
await xfs.mktempPromise(async cwd => {
1659+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {});
1660+
1661+
await xfs.writeFilePromise(ppath.join(cwd, `.corepack.env` as Filename), `COREPACK_ON_UNVERIFIED_DOWNLOAD=error\n`);
1662+
1663+
// By default, Corepack should be using .corepack.env (or the built-in ones on Node.js 18.x) and fail.
1664+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1665+
exitCode: 1,
1666+
stdout: ``,
1667+
stderr: expect.stringContaining(`Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable`),
1668+
});
1669+
1670+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
1671+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
1672+
exitCode: 0,
1673+
stdout: `pnpm: Hello from custom registry\n`,
1674+
stderr: expect.stringContaining(`Integrity of pnpm@1.9998.9999 could not be verified`),
1675+
});
1676+
});
1677+
});
1678+
});
1679+
1680+
describe(`downloads not pinned by a hash`, () => {
1681+
it(`should not warn in non-strict mode when the signature can be verified`, async () => {
1682+
await xfs.mktempPromise(async cwd => {
1683+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1684+
packageManager: `yarn@1.22.4`,
1685+
});
1686+
1687+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `warn`;
1688+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
1689+
exitCode: 0,
1690+
stdout: `1.22.4\n`,
1691+
stderr: ``,
1692+
});
1693+
});
1694+
});
1695+
1696+
it(`should warn when COREPACK_ON_UNVERIFIED_DOWNLOAD is set to strict-warn`, async () => {
1697+
await xfs.mktempPromise(async cwd => {
1698+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1699+
packageManager: `yarn@1.22.4`,
1700+
});
1701+
1702+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `strict-warn`;
1703+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
1704+
exitCode: 0,
1705+
stdout: `1.22.4\n`,
1706+
stderr: `Integrity of yarn@1.22.4 is not pinned by a hash. Consider providing a hash. Set COREPACK_ON_UNVERIFIED_DOWNLOAD to 'ignore' to remove this warning.\n`,
1707+
});
1708+
});
1709+
});
1710+
1711+
it(`should fail when COREPACK_ON_UNVERIFIED_DOWNLOAD is set to STRICT-ERROR`, async () => {
1712+
await xfs.mktempPromise(async cwd => {
1713+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1714+
packageManager: `yarn@1.22.4`,
1715+
});
1716+
1717+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `STRICT-ERROR`;
1718+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
1719+
exitCode: 1,
1720+
stdout: ``,
1721+
stderr: expect.stringContaining(`Integrity of yarn@1.22.4 is not pinned by a hash. Downloading unverified versions is disabled by the COREPACK_ON_UNVERIFIED_DOWNLOAD env variable.`),
1722+
});
1723+
});
1724+
});
1725+
1726+
it(`should not interfere with versions pinned by a hash in strict mode`, async () => {
1727+
await xfs.mktempPromise(async cwd => {
1728+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
1729+
packageManager: `yarn@1.22.4+sha224.0d6eecaf4d82ec12566fdd97143794d0f0c317e0d652bd4d1b305430`,
1730+
});
1731+
1732+
process.env.COREPACK_ON_UNVERIFIED_DOWNLOAD = `strict-error`;
1733+
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
1734+
exitCode: 0,
1735+
stdout: `1.22.4\n`,
1736+
stderr: ``,
1737+
});
1738+
});
1739+
});
1740+
});
1741+
15521742
for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK_NPM_PASSWORD`, `PROXY`]) {
15531743
describe(`custom registry with auth ${authType}`, () => {
15541744
beforeEach(() => {
@@ -1741,8 +1931,8 @@ describe(`handle integrity checks`, () => {
17411931
});
17421932
await expect(runCli(cwd, [`use`, `pnpm`], true)).resolves.toMatchObject({
17431933
exitCode: 1,
1744-
stdout: expect.stringContaining(`Signature does not match`),
1745-
stderr: ``,
1934+
stderr: expect.stringContaining(`Signature does not match`),
1935+
stdout: `Installing pnpm@1.9998.9999 in the project...\n`,
17461936
});
17471937
});
17481938
});
@@ -1757,8 +1947,8 @@ describe(`handle integrity checks`, () => {
17571947
});
17581948
await expect(runCli(cwd, [`use`, `yarn@1.9998.9999`], true)).resolves.toMatchObject({
17591949
exitCode: 1,
1760-
stdout: expect.stringContaining(`Signature does not match`),
1761-
stderr: ``,
1950+
stderr: expect.stringContaining(`Signature does not match`),
1951+
stdout: `Installing yarn@1.9998.9999 in the project...\n`,
17621952
});
17631953
});
17641954
});

0 commit comments

Comments
 (0)