Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/utils/error-message.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const errorMessage = (er, npm) => {
}

case 'EALLOWGIT':
case 'EALLOWREMOTE':
summary.push(['', er.message])
detail.push(['', `Refusing to fetch "${er.package}"`])
break
Expand Down
8 changes: 4 additions & 4 deletions tap-snapshots/test/lib/commands/config.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna
"allow-same-version": false,
"allow-directory": "all",
"allow-file": "all",
"allow-git": "all",
"allow-remote": "all",
"allow-git": "none",
"allow-remote": "none",
"allow-scripts": [
""
],
Expand Down Expand Up @@ -201,8 +201,8 @@ access = null
all = false
allow-directory = "all"
allow-file = "all"
allow-git = "all"
allow-remote = "all"
allow-git = "none"
allow-remote = "none"
allow-same-version = false
allow-scripts = [""]
allow-scripts-pending = false
Expand Down
20 changes: 16 additions & 4 deletions tap-snapshots/test/lib/docs.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ dependencies to be used for other commands like \`npm view\`

#### \`allow-git\`

* Default: "all"
* Default: "none"
* Type: "all", "none", or "root"

Limits the ability for npm to fetch dependencies from git references. That
Expand All @@ -264,6 +264,11 @@ range. Please note that this could leave your tree incomplete and some
packages may not function as intended or designed. Changing this setting
will not remove dependencies that are already installed.

As of npm 12 the default is \`none\`. Git dependencies run \`git\` against a
remote repo and may install configuration the project does not control. Opt
in explicitly per project (in \`.npmrc\`) or per command (on the CLI) when you
need git deps.

\`all\` allows any git dependencies to be fetched and installed. \`none\`
prevents any git dependencies from being fetched and installed. \`root\` only
allows git dependencies defined in your project's package.json to be fetched
Expand All @@ -274,7 +279,7 @@ like \`npm view\`

#### \`allow-remote\`

* Default: "all"
* Default: "none"
* Type: "all", "none", or "root"

Limits the ability for npm to fetch dependencies from urls. That is,
Expand All @@ -283,6 +288,13 @@ range. Please note that this could leave your tree incomplete and some
packages may not function as intended or designed. Changing this setting
will not remove dependencies that are already installed.

As of npm 12 the default is \`none\`. Tarballs that share a hostname with the
configured registry (the typical case for the npm registry, GitHub Packages,
and most private registries) are still installed normally. If your registry
serves tarballs from a different host, set \`replace-registry-host\` or
override this setting. Opt in explicitly per project (in \`.npmrc\`) or per
command (on the CLI) when you intentionally install from a URL.

\`all\` allows any url to be installed. \`none\` prevents any url from being
installed. \`root\` only allows urls defined in your project's package.json to
be installed. Also allows url dependencies to be used for other commands
Expand Down Expand Up @@ -2778,8 +2790,8 @@ Object {
"all": false,
"allowDirectory": "all",
"allowFile": "all",
"allowGit": "all",
"allowRemote": "all",
"allowGit": "none",
"allowRemote": "none",
"allowSameVersion": false,
"allowScripts": Array [],
"allowScriptsPending": false,
Expand Down
1 change: 1 addition & 0 deletions test/lib/commands/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ t.test('packs from git spec', async t => {
config: {
audit: false,
yes: true,
'allow-git': 'all',
},
})
try {
Expand Down
32 changes: 32 additions & 0 deletions test/lib/commands/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,20 @@ t.test('exec commands', async t => {
)
})

t.test('allow-git default rejects git deps', async t => {
const { npm } = await loadMockNpm(t, {
config: { audit: false },
})
await t.rejects(
npm.exec('install', ['npm/npm']),
{
code: 'EALLOWGIT',
package: 'github:npm/npm',
},
'no explicit allow-git config still blocks git installs'
)
})

t.test('allow-git=root refuses non-root git dependency', async t => {
const { npm } = await loadMockNpm(t, {
config: {
Expand Down Expand Up @@ -507,6 +521,24 @@ t.test('exec commands', async t => {
'user-supplied remote URL is still blocked'
)
})

t.test('allow-remote default rejects a user-supplied remote URL', async t => {
const { npm } = await loadMockNpm(t, {
config: { audit: false },
prefixDir: {
'package.json': JSON.stringify({
name: '@npmcli/test-package',
version: '1.0.0',
dependencies: { abbrev: 'https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz' },
}),
},
})
await t.rejects(
npm.exec('install', []),
{ code: 'EALLOWREMOTE' },
'no explicit allow-remote config still blocks user-supplied tarball URLs'
)
})
})

t.test('completion', async t => {
Expand Down
4 changes: 3 additions & 1 deletion test/lib/commands/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,9 @@ const loadMockNpm = async function (t, opts = {}) {
}

t.test('package from git', async t => {
const { view, joinedOutput } = await loadMockNpm(t, { config: { unicode: false } })
const { view, joinedOutput } = await loadMockNpm(t, {
config: { unicode: false, 'allow-git': 'all' },
})
await view.exec(['https://github.com/npm/green'])
t.matchSnapshot(joinedOutput())
})
Expand Down
17 changes: 15 additions & 2 deletions workspaces/config/lib/definitions/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,29 +225,42 @@ const definitions = {
flatten,
}),
'allow-git': new Definition('allow-git', {
default: 'all',
default: 'none',
type: ['all', 'none', 'root'],
description: `
Limits the ability for npm to fetch dependencies from git references.
That is, dependencies that point to a git repo instead of a version or semver range.
Please note that this could leave your tree incomplete and some packages may not function as intended or designed.
Changing this setting will not remove dependencies that are already installed.

As of npm 12 the default is \`none\`. Git dependencies run \`git\`
against a remote repo and may install configuration the project does
not control. Opt in explicitly per project (in \`.npmrc\`) or per
command (on the CLI) when you need git deps.

\`all\` allows any git dependencies to be fetched and installed.
\`none\` prevents any git dependencies from being fetched and installed.
\`root\` only allows git dependencies defined in your project's package.json to be fetched and installed. Also allows git dependencies to be fetched for other commands like \`npm view\`
`,
flatten,
}),
'allow-remote': new Definition('allow-remote', {
default: 'all',
default: 'none',
type: ['all', 'none', 'root'],
description: `
Limits the ability for npm to fetch dependencies from urls.
That is, dependencies that point to a tarball url instead of a version or semver range.
Please note that this could leave your tree incomplete and some packages may not function as intended or designed.
Changing this setting will not remove dependencies that are already installed.

As of npm 12 the default is \`none\`. Tarballs that share a hostname
with the configured registry (the typical case for the npm registry,
GitHub Packages, and most private registries) are still installed
normally. If your registry serves tarballs from a different host,
set \`replace-registry-host\` or override this setting. Opt in
explicitly per project (in \`.npmrc\`) or per command (on the CLI)
when you intentionally install from a URL.

\`all\` allows any url to be installed.
\`none\` prevents any url from being installed.
\`root\` only allows urls defined in your project's package.json to be installed. Also allows url dependencies to be used for other commands like \`npm view\`
Expand Down
27 changes: 27 additions & 0 deletions workspaces/config/test/definitions/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,33 @@ t.test('node-gyp', t => {
t.end()
})

t.test('allow-git defaults to none and flattens to allowGit', t => {
const defs = mockDefs()
t.equal(defs['allow-git'].default, 'none')
t.strictSame(defs['allow-git'].type, ['all', 'none', 'root'])
const flat = {}
defs['allow-git'].flatten('allow-git', { 'allow-git': 'root' }, flat)
t.strictSame(flat, { allowGit: 'root' })
t.end()
})

t.test('allow-remote defaults to none and flattens to allowRemote', t => {
const defs = mockDefs()
t.equal(defs['allow-remote'].default, 'none')
t.strictSame(defs['allow-remote'].type, ['all', 'none', 'root'])
const flat = {}
defs['allow-remote'].flatten('allow-remote', { 'allow-remote': 'all' }, flat)
t.strictSame(flat, { allowRemote: 'all' })
t.end()
})

t.test('allow-file and allow-directory still default to all', t => {
const defs = mockDefs()
t.equal(defs['allow-file'].default, 'all', 'allow-file unchanged')
t.equal(defs['allow-directory'].default, 'all', 'allow-directory unchanged')
t.end()
})

t.test('allow-scripts', t => {
t.test('defaults to empty string and flattens to []', t => {
const defs = mockDefs()
Expand Down
19 changes: 15 additions & 4 deletions workspaces/libnpmdiff/lib/tarball.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,21 @@ const tarball = (manifest, opts) => {
return nodeModulesTarball(manifest, opts)
}

return pacote.tarball(manifest._resolved, {
...opts,
Arborist,
})
// pacote re-parses manifest._resolved as type=remote, so allow-remote=none
// would mis-fire on the tarball URL the registry just handed us. mirror
// arborist reify's carve-out: trust resolved tarballs for registry-typed specs.
const tarballOpts = { ...opts, Arborist }
let fromSpec
try {
fromSpec = manifest._from ? npa(manifest._from) : null
} catch {
fromSpec = null
}
if (fromSpec?.registry) {
tarballOpts.allowRemote = 'all'
}

return pacote.tarball(manifest._resolved, tarballOpts)
}

module.exports = tarball
48 changes: 48 additions & 0 deletions workspaces/libnpmdiff/test/tarball.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,51 @@ t.test('pkg not in a node_modules folder', async t => {
'should call regular pacote.tarball method instead'
)
})

t.test('allowRemote carve-out for registry-mediated tarballs', async t => {
const originalTarball = pacote.tarball
let captured
pacote.tarball = async (resolved, opts) => {
captured = { resolved, opts }
return Buffer.alloc(0)
}
t.teardown(() => {
pacote.tarball = originalTarball
})

t.beforeEach(() => {
captured = undefined
})

t.test('registry-typed _from sets allowRemote=all', async t => {
await tarball({
_from: 'abbrev@1.0.0',
_resolved: 'https://registry.npmjs.org/abbrev/-/abbrev-1.0.0.tgz',
}, {})
t.equal(captured.opts.allowRemote, 'all')
})

t.test('non-registry _from leaves allowRemote untouched', async t => {
await tarball({
_from: 'https://example.com/foo.tgz',
_resolved: 'https://example.com/foo.tgz',
}, {})
t.notOk(captured.opts.allowRemote)
})

t.test('missing _from leaves allowRemote untouched', async t => {
await tarball({
_resolved: 'https://example.com/foo.tgz',
}, {})
t.notOk(captured.opts.allowRemote)
})

t.test('unparseable _from leaves allowRemote untouched', async t => {
// npa throws on tags with reserved characters, exercising the catch branch
await tarball({
_from: '@',
_resolved: 'https://example.com/foo.tgz',
}, {})
t.notOk(captured.opts.allowRemote)
})
})
4 changes: 3 additions & 1 deletion workspaces/libnpmexec/lib/run-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const run = async ({
// necessary for preventing bash/cmd keywords from overriding
if (!isWindowsShell) {
if (args.length > 0) {
args[0] = '"' + args[0] + '"'
// single-quote so shell metacharacters in the executable name are taken
// literally; double quotes still expand $(), backticks, $var and "
args[0] = `'${args[0].replace(/'/g, `'\\''`)}'`
}
}

Expand Down
14 changes: 14 additions & 0 deletions workspaces/libnpmexec/test/run-script.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ t.test('isWindows', async t => {
await runScript()
})

t.test('escapes executable name to neutralize shell metacharacters', async t => {
let pkg
const { runScript } = await mockRunScript(t, {
'ci-info': { isCI: true },
'@npmcli/run-script': async (opts) => {
pkg = opts.pkg
},
'../lib/is-windows.js': false,
})

await runScript({ args: [`evil'; touch pwned #`] })
t.equal(pkg.scripts.npx, `'evil'\\''; touch pwned #'`)
})

t.test('isNotWindows', async t => {
const { runScript } = await mockRunScript(t, {
'ci-info': { isCI: true },
Expand Down
13 changes: 10 additions & 3 deletions workspaces/libnpmpack/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,23 @@ async function pack (spec = 'file:.', opts = {}) {
}

// packs tarball
const tarball = await pacote.tarball(manifest._resolved, {
const tarballOpts = {
...opts,
Arborist,
integrity: manifest._integrity,
})
}
// pacote re-parses manifest._resolved as type=remote, so allow-remote=none
// would mis-fire on the tarball URL the registry just handed us. mirror
// arborist reify's carve-out: trust resolved tarballs for registry-typed specs.
if (spec.registry) {
tarballOpts.allowRemote = 'all'
}
const tarball = await pacote.tarball(manifest._resolved, tarballOpts)

// check for explicit `false` so the default behavior is to skip writing to disk
if (opts.dryRun === false) {
const filename = `${manifest.name}-${manifest.version}.tgz`
.replace(/^@/, '').replace(/\//, '-')
.replace(/^@/, '').replace(/[/\\]/g, '-')
const destination = path.resolve(opts.packDestination, filename)
await writeFile(destination, tarball)
}
Expand Down
23 changes: 23 additions & 0 deletions workspaces/libnpmpack/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,29 @@ t.test('packs from local directory', async t => {
})
})

t.test('flattens path separators in name so tarball stays in packDestination', async t => {
const testDir = t.testdir({
src: {
'package.json': JSON.stringify({
name: 'x/../../../../../../escaped',
version: '1.0.0',
}, null, 2),
},
dest: {},
})

const dest = path.join(testDir, 'dest')
await pack(`file:${path.join(testDir, 'src')}`, {
dryRun: false,
packDestination: dest,
silent: true,
})

const written = fs.readdirSync(dest)
t.same(written, ['x-..-..-..-..-..-..-escaped-1.0.0.tgz'], 'separators flattened to a single filename')
t.notOk(fs.existsSync(path.join(testDir, 'escaped-1.0.0.tgz')), 'nothing escaped the destination')
})

t.test('writes tarball to file when dryRun === false', async t => {
const testDir = t.testdir({
'package.json': JSON.stringify({
Expand Down
Loading