diff --git a/lib/commands/view.js b/lib/commands/view.js index 747f1d4ffafd8..b43766936b042 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -385,11 +385,19 @@ function parseArgs (args) { } } +// keyed by version.version, which is registry-controlled; the same set +// lib/utils/queryable.js refuses so a crafted manifest can't walk up to +// Object.prototype through acc[k][t] +const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']) + function cleanData (obj, wholePackument) { // JSON formatted output (JSON or specific attributes from packument) const data = obj.reduce((acc, cur) => { if (cur) { Object.entries(cur).forEach(([k, v]) => { + if (FORBIDDEN_KEYS.has(k)) { + return + } acc[k] ||= {} Object.keys(v).forEach((t) => { acc[k][t] = cur[k][t] diff --git a/test/lib/commands/view.js b/test/lib/commands/view.js index 3b5a581f3a6b1..9f398d728e7f1 100644 --- a/test/lib/commands/view.js +++ b/test/lib/commands/view.js @@ -525,6 +525,34 @@ t.test('package with --json and no versions', async t => { t.equal(joinedOutput(), '', 'no info to display') }) +t.test('crafted version field does not pollute Object.prototype', async t => { + // A malicious registry can serve a manifest whose own `version` field + // differs from the (valid semver) versions map key. cleanData keys its + // accumulator by that version field, so a value of `__proto__` used to + // walk up to Object.prototype through acc[k][t]. + t.teardown(() => { + delete Object.prototype.polluted + }) + const evilPackument = () => ({ + _id: 'evil@1.0.0', + name: 'evil', + 'dist-tags': { latest: '1.0.0' }, + versions: { + '1.0.0': { + name: 'evil', + version: '__proto__', + polluted: 'HACKED', + dist: { shasum: '123', tarball: 'http://hm.evil.com/1.0.0.tgz', fileCount: 1 }, + }, + }, + }) + const { view } = await loadMockNpm(t, { + mocks: { pacote: { packument: evilPackument } }, + }) + await view.exec(['evil', 'polluted']) + t.equal({}.polluted, undefined, 'Object.prototype is not polluted') +}) + t.test('package with --json and single string arg', async t => { const { view, joinedOutput } = await loadMockNpm(t, { config: { json: true } }) await view.exec(['blue', 'dist-tags.latest'])