Skip to content

Commit fa74645

Browse files
authored
Validation fix (#7582)
* Validates both checksum and integrity * Adds a test * Revert "Fixes the problem another way" This reverts commit 29a8c58. * Revert "Fixes tests" This reverts commit 9322e76. * Revert "Fixes flow linting" This reverts commit 431a9e9. * Fixes flow * Back to v5 we are
1 parent 29a8c58 commit fa74645

File tree

11 files changed

+89
-112
lines changed

11 files changed

+89
-112
lines changed

__tests__/commands/install/integration.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ test('changes the cache path when bumping the cache version', () =>
245245
});
246246
}));
247247

248-
test.skip('changes the cache directory when bumping the cache version', () =>
248+
test('changes the cache directory when bumping the cache version', () =>
249249
runInstall({}, 'install-production', async (config, reporter): Promise<void> => {
250250
const lockfile = await Lockfile.fromDirectory(config.cwd);
251251

@@ -632,6 +632,11 @@ test('install should be idempotent', () =>
632632
null,
633633
));
634634

635+
test('install should fail to authenticate integrity with incorrect hash and correct sha512', () =>
636+
expect(runInstall({}, 'invalid-checksum-good-integrity')).rejects.toMatchObject({
637+
message: expect.stringContaining("computed integrity doesn't match our records"),
638+
}));
639+
635640
test('install should authenticate integrity field with sha1 checksums', () =>
636641
runInstall({}, 'install-update-auth-sha1', async config => {
637642
const lockFileContent = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
@@ -795,7 +800,7 @@ test('install should fail with unsupported algorithms', () =>
795800
message: expect.stringContaining('none of the specified algorithms are supported'),
796801
}));
797802

798-
test('install should update integrity in yarn.lock (--update-checksums)', () =>
803+
test.concurrent('install should update integrity in yarn.lock (--update-checksums)', () =>
799804
runInstall({updateChecksums: true}, 'install-update-checksums', async config => {
800805
const lockFileLines = explodeLockfile(await fs.readFile(path.join(config.cwd, 'yarn.lock')));
801806
expect(lockFileLines[3]).toEqual(
@@ -806,7 +811,7 @@ test('install should update integrity in yarn.lock (--update-checksums)', () =>
806811
}),
807812
);
808813

809-
test('install should update malformed integrity string in yarn.lock (--update-checksums)', () =>
814+
test.concurrent('install should update malformed integrity string in yarn.lock (--update-checksums)', () =>
810815
runInstall({updateChecksums: true}, 'install-update-checksums-malformed', async config => {
811816
const lockFileLines = explodeLockfile(await fs.readFile(path.join(config.cwd, 'yarn.lock')));
812817
expect(lockFileLines[3]).toEqual(

__tests__/fixtures/install/install-update-auth-sha1/yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
abab@^1.0.4:
66
version "1.0.4"
7-
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#foo"
7+
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
88
integrity sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=
99

1010
leftpad@^0.0.1:
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "badpkg",
3+
"version": "1.0.0",
4+
"description": "A bad package",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "UNLICENSED",
11+
"dependencies": {
12+
"express": "4.11.1"
13+
}
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2+
# yarn lockfile v1
3+
4+
5+
6+
version "4.11.1"
7+
resolved "https://registry.yarnpkg.com/ponyhooves/-/ponyhooves-1.0.1.tgz#36d04dd27aa1667634e987529767f9c99de7903f"
8+
integrity sha1-5XycPpdtVw+X8ik1bKXW7hPv01g=

__tests__/fixtures/request-cache/GET/gitlab.com/leanlabsio/kanban/raw/7f21696fb9d08130dd62abd96c9572f513c05301/package.json.bin

Lines changed: 0 additions & 61 deletions
This file was deleted.

__tests__/fixtures/request-cache/GET/registry.yarnpkg.com/is-pnp.bin

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/config.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {registries, registryNames} from './registries/index.js';
1717
import {NoopReporter} from './reporters/index.js';
1818
import map from './util/map.js';
1919

20-
const crypto = require('crypto');
2120
const detectIndent = require('detect-indent');
2221
const invariant = require('invariant');
2322
const path = require('path');
@@ -509,18 +508,14 @@ export default class Config {
509508
slug = `unknown-${slug}`;
510509
}
511510

512-
const {hash, resolved} = pkg.remote;
511+
const {hash} = pkg.remote;
513512

514513
if (pkg.version) {
515514
slug += `-${pkg.version}`;
516515
}
517516

518-
if (resolved) {
519-
if (hash) {
520-
slug += `-${crypto.createHmac('sha1', resolved).update(hash).digest('hex')}`;
521-
} else {
522-
slug += `-${crypto.createHash('sha1').update(resolved).digest('hex')}`;
523-
}
517+
if (pkg.uid && pkg.version !== pkg.uid) {
518+
slug += `-${pkg.uid}`;
524519
} else if (hash) {
525520
slug += `-${hash}`;
526521
}

src/fetchers/tarball-fetcher.js

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,12 @@ export default class TarballFetcher extends BaseFetcher {
8888
reject: (error: Error) => void,
8989
tarballPath?: string,
9090
): {
91-
validateStream: ssri.integrityStream,
91+
hashValidateStream: stream.PassThrough,
92+
integrityValidateStream: stream.PassThrough,
9293
extractorStream: stream.Transform,
9394
} {
94-
const integrityInfo = this._supportedIntegrity();
95+
const hashInfo = this._supportedIntegrity({hashOnly: true});
96+
const integrityInfo = this._supportedIntegrity({hashOnly: false});
9597

9698
const now = new Date();
9799

@@ -124,7 +126,9 @@ export default class TarballFetcher extends BaseFetcher {
124126
},
125127
});
126128

127-
const validateStream = new ssri.integrityStream(integrityInfo);
129+
const hashValidateStream = new ssri.integrityStream(hashInfo);
130+
const integrityValidateStream = new ssri.integrityStream(integrityInfo);
131+
128132
const untarStream = tarFs.extract(this.dest, {
129133
strip: 1,
130134
dmode: 0o755, // all dirs should be readable
@@ -138,10 +142,13 @@ export default class TarballFetcher extends BaseFetcher {
138142
});
139143
const extractorStream = gunzip();
140144

141-
validateStream.once('error', err => {
145+
hashValidateStream.once('error', err => {
146+
this.validateError = err;
147+
});
148+
integrityValidateStream.once('error', err => {
142149
this.validateError = err;
143150
});
144-
validateStream.once('integrity', sri => {
151+
integrityValidateStream.once('integrity', sri => {
145152
this.validateIntegrity = sri;
146153
});
147154

@@ -192,7 +199,7 @@ export default class TarballFetcher extends BaseFetcher {
192199
});
193200
});
194201

195-
return {validateStream, extractorStream};
202+
return {hashValidateStream, integrityValidateStream, extractorStream};
196203
}
197204

198205
getLocalPaths(override: ?string): Array<string> {
@@ -217,9 +224,16 @@ export default class TarballFetcher extends BaseFetcher {
217224
invariant(stream, 'stream should be available at this point');
218225
// $FlowFixMe - This is available https://nodejs.org/api/fs.html#fs_readstream_path
219226
const tarballPath = stream.path;
220-
const {validateStream, extractorStream} = this.createExtractor(resolve, reject, tarballPath);
227+
const {hashValidateStream, integrityValidateStream, extractorStream} = this.createExtractor(
228+
resolve,
229+
reject,
230+
tarballPath,
231+
);
232+
233+
stream.pipe(hashValidateStream);
234+
hashValidateStream.pipe(integrityValidateStream);
221235

222-
stream.pipe(validateStream).pipe(extractorStream).on('error', err => {
236+
integrityValidateStream.pipe(extractorStream).on('error', err => {
223237
reject(new MessageError(this.config.reporter.lang('fetchErrorCorrupt', err.message, tarballPath)));
224238
});
225239
});
@@ -243,19 +257,23 @@ export default class TarballFetcher extends BaseFetcher {
243257
const tarballMirrorPath = this.getTarballMirrorPath();
244258
const tarballCachePath = this.getTarballCachePath();
245259

246-
const {validateStream, extractorStream} = this.createExtractor(resolve, reject);
260+
const {hashValidateStream, integrityValidateStream, extractorStream} = this.createExtractor(
261+
resolve,
262+
reject,
263+
);
247264

248-
req.pipe(validateStream);
265+
req.pipe(hashValidateStream);
266+
hashValidateStream.pipe(integrityValidateStream);
249267

250268
if (tarballMirrorPath) {
251-
validateStream.pipe(fs.createWriteStream(tarballMirrorPath)).on('error', reject);
269+
integrityValidateStream.pipe(fs.createWriteStream(tarballMirrorPath)).on('error', reject);
252270
}
253271

254272
if (tarballCachePath) {
255-
validateStream.pipe(fs.createWriteStream(tarballCachePath)).on('error', reject);
273+
integrityValidateStream.pipe(fs.createWriteStream(tarballCachePath)).on('error', reject);
256274
}
257275

258-
validateStream.pipe(extractorStream).on('error', reject);
276+
integrityValidateStream.pipe(extractorStream).on('error', reject);
259277
},
260278
},
261279
this.packageName,
@@ -311,8 +329,8 @@ export default class TarballFetcher extends BaseFetcher {
311329
return this.fetchFromLocal().catch(err => this.fetchFromExternal());
312330
}
313331

314-
_findIntegrity(): ?Object {
315-
if (this.remote.integrity) {
332+
_findIntegrity({hashOnly}: {hashOnly: boolean}): ?Object {
333+
if (this.remote.integrity && !hashOnly) {
316334
return ssri.parse(this.remote.integrity);
317335
}
318336
if (this.hash) {
@@ -321,12 +339,12 @@ export default class TarballFetcher extends BaseFetcher {
321339
return null;
322340
}
323341

324-
_supportedIntegrity(): {integrity: ?Object, algorithms: Array<string>} {
325-
const expectedIntegrity = this._findIntegrity() || {};
342+
_supportedIntegrity({hashOnly}: {hashOnly: boolean}): {integrity: ?Object, algorithms: Array<string>} {
343+
const expectedIntegrity = this._findIntegrity({hashOnly}) || {};
326344
const expectedIntegrityAlgorithms = Object.keys(expectedIntegrity);
327345
const shouldValidateIntegrity = (this.hash || this.remote.integrity) && !this.config.updateChecksums;
328346

329-
if (expectedIntegrityAlgorithms.length === 0 && !shouldValidateIntegrity) {
347+
if (expectedIntegrityAlgorithms.length === 0 && (!shouldValidateIntegrity || hashOnly)) {
330348
const algorithms = this.config.updateChecksums ? ['sha512'] : ['sha1'];
331349
// for consistency, return sha1 for packages without a remote integrity (eg. github)
332350
return {integrity: null, algorithms};

src/package-fetcher.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,24 @@ import * as fetchers from './fetchers/index.js';
99
import * as fs from './util/fs.js';
1010
import * as promise from './util/promise.js';
1111

12-
async function fetchCache(dest: string, fetcher: Fetchers, config: Config): Promise<FetchedMetadata> {
13-
const {hash, package: pkg} = await config.readPackageMetadata(dest);
12+
const ssri = require('ssri');
13+
14+
async function fetchCache(
15+
dest: string,
16+
fetcher: Fetchers,
17+
config: Config,
18+
integrity: ?string,
19+
): Promise<FetchedMetadata> {
20+
// $FlowFixMe: This error doesn't make sense
21+
const {hash, package: pkg, remote} = await config.readPackageMetadata(dest);
22+
23+
if (integrity) {
24+
if (!remote.integrity || !ssri.parse(integrity).match(remote.integrity)) {
25+
// eslint-disable-next-line yarn-internal/warn-language
26+
throw new MessageError('Incorrect integrity when fetching from the cache');
27+
}
28+
}
29+
1430
await fetcher.setupMirrorFromCache();
1531
return {
1632
package: pkg,
@@ -40,7 +56,7 @@ export async function fetchOneRemote(
4056

4157
const fetcher = new Fetcher(dest, remote, config);
4258
if (await config.isValidModuleDest(dest)) {
43-
return fetchCache(dest, fetcher, config);
59+
return fetchCache(dest, fetcher, config, remote.integrity);
4460
}
4561

4662
// remove as the module may be invalid

0 commit comments

Comments
 (0)