Skip to content

Commit 5cd5150

Browse files
JamieMageeowlstronaut
authored andcommitted
feat: default-deny install scripts (allowScripts opt-in) [v12]
Phase 2 of the RFC #868 install-script policy: flip the default so unreviewed lifecycle scripts are blocked unless covered by allowScripts. Stacked on the behavior-neutral tooling PR; this commit carries ONLY the v12-only default flip: - arborist: gate preinstall/install/postinstall/prepare in rebuild on the allowScripts policy (default-deny) - user-facing "blocked because not covered by allowScripts" wording in rebuild/reify-output/allow-scripts-cmd - config definition docs + approve/deny command docs + snapshots - flip tests
1 parent 64e3f79 commit 5cd5150

16 files changed

Lines changed: 162 additions & 74 deletions

File tree

docs/lib/content/commands/npm-approve-scripts.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ records which of your dependencies are permitted to run install scripts
1515
(`preinstall`, `install`, `postinstall`, and `prepare` for non-registry
1616
sources). This command is the recommended way to maintain that field.
1717

18-
In the current release, this field is advisory: install scripts still run
19-
by default, but installs print a list of packages whose scripts have not
20-
been reviewed. A future release will block unreviewed install scripts.
18+
Dependency install scripts are blocked by default. Install commands
19+
silently skip lifecycle scripts for any dependency that does not have a
20+
matching entry in `allowScripts`, and end with a list of the packages
21+
whose scripts were skipped so you can review them with this command.
2122

2223
This command only works inside a project that has a `package.json`. It does
2324
not apply to global installs (`npm install -g`) or one-off executions

docs/lib/content/commands/npm-deny-scripts.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Writes `false` entries into the `allowScripts` field of your project's
1515
`package.json`, recording that a dependency must not run install scripts
1616
even if a future version would otherwise be eligible.
1717

18-
In the current release, install scripts still run by default, so `deny-scripts`
19-
only affects how installs of denied packages are reported. A future release
20-
will block unreviewed install scripts and respect deny entries at install
21-
time.
18+
Dependency install scripts are blocked by default. Adding a `false`
19+
entry with `deny-scripts` makes the denial explicit (so it survives
20+
`npm approve-scripts --all`) and excludes the package from any future
21+
`--allow-scripts-pending` review prompts.
2222

2323
```bash
2424
npm deny-scripts <pkg> [<pkg> ...]

lib/commands/rebuild.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,12 @@ class Rebuild extends ArboristWorkspaceCmd {
6868
await arb.rebuild()
6969
}
7070

71-
// Phase 1 advisory: list any packages whose install scripts ran (or
72-
// would have run) and are not yet covered by allowScripts. Rebuild
73-
// doesn't go through reifyFinish, so the walker is invoked here.
71+
// Rebuild skips reifyFinish, so run the walker here to list any
72+
// packages whose install scripts were blocked.
7473
const unreviewed = await checkAllowScripts({ arb, npm: this.npm })
7574
if (unreviewed.length > 0) {
7675
const count = unreviewed.length
77-
const noun = count === 1 ? 'package has' : 'packages have'
76+
const noun = count === 1 ? 'package had' : 'packages had'
7877
// `npm approve-scripts` writes to a project package.json, which doesn't
7978
// exist for global rebuilds. Point global users at `npm config set`,
8079
// which writes the `allow-scripts` setting to their user .npmrc.
@@ -84,7 +83,7 @@ class Rebuild extends ArboristWorkspaceCmd {
8483
: 'Run `npm approve-scripts --allow-scripts-pending` to review.'
8584
log.warn(
8685
'rebuild',
87-
`${count} ${noun} install scripts not yet covered by allowScripts. ` +
86+
`${count} ${noun} install scripts blocked because they are not covered by allowScripts. ` +
8887
remediation
8988
)
9089
}

lib/utils/allow-scripts-cmd.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class AllowScriptsCmd extends BaseCommand {
8787
const has = count === 1 ? 'has' : 'have'
8888
const pkg = count === 1 ? 'package' : 'packages'
8989
output.standard(
90-
`${count} ${pkg} ${has} install scripts not yet covered by allowScripts:`
90+
`${count} ${pkg} ${has} install scripts blocked because they are not covered by allowScripts:`
9191
)
9292
for (const { node, scripts } of unreviewed) {
9393
const { name, version } = trustedDisplay(node)

lib/utils/reify-output.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,9 @@ const unreviewedScriptsMessage = (npm, unreviewedScripts) => {
241241
// stdout is reserved for things the user explicitly asked to see
242242
// (npm ls, npm view).
243243
const count = unreviewedScripts.length
244-
const pkg = count === 1 ? 'package has' : 'packages have'
245-
const header = `${count} ${pkg} install scripts not yet covered by allowScripts:`
244+
const pkg = count === 1 ? 'package had' : 'packages had'
245+
const header =
246+
`${count} ${pkg} install scripts blocked because they are not covered by allowScripts:`
246247

247248
const names = []
248249
const lines = unreviewedScripts.map(({ node, scripts }) => {

smoke-tests/tap-snapshots/test/index.js.test.cjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ npm error --allow-scripts
107107
npm error Comma-separated list of packages whose install-time lifecycle scripts
108108
npm error
109109
npm error --strict-allow-scripts
110-
npm error If \`true\`, turn the install-script policy from a warning into a hard
110+
npm error If \`true\`, turn the install-script policy from a silent skip into a
111111
npm error
112112
npm error --dangerously-allow-all-scripts
113113
npm error If \`true\`, bypass the \`allowScripts\` policy entirely and run every

tap-snapshots/test/lib/docs.js.test.cjs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,12 +1912,13 @@ this to work properly.
19121912
* Default: false
19131913
* Type: Boolean
19141914
1915-
If \`true\`, turn the install-script policy from a warning into a hard error:
1916-
any dependency with install scripts not covered by \`allowScripts\` will fail
1917-
the install instead of running with a notice.
1915+
If \`true\`, turn the install-script policy from a silent skip into a hard
1916+
error: any dependency with install scripts not covered by \`allowScripts\`
1917+
will fail the install instead of being silently skipped.
19181918
1919-
Dependencies explicitly denied with \`false\` in \`allowScripts\` are always
1920-
silently skipped; this setting only affects unreviewed entries.
1919+
By default, dependencies whose install scripts are not approved in
1920+
\`allowScripts\` are silently skipped; this setting promotes that silent skip
1921+
into a hard failure, which is the recommended posture for CI.
19211922
\`--ignore-scripts\` and \`--dangerously-allow-all-scripts\` both override this
19221923
setting.
19231924
@@ -3283,7 +3284,7 @@ Options:
32833284
Comma-separated list of packages whose install-time lifecycle scripts
32843285
32853286
--strict-allow-scripts
3286-
If \`true\`, turn the install-script policy from a warning into a hard
3287+
If \`true\`, turn the install-script policy from a silent skip into a
32873288
32883289
--dangerously-allow-all-scripts
32893290
If \`true\`, bypass the \`allowScripts\` policy entirely and run every
@@ -3845,7 +3846,7 @@ Options:
38453846
Comma-separated list of packages whose install-time lifecycle scripts
38463847
38473848
--strict-allow-scripts
3848-
If \`true\`, turn the install-script policy from a warning into a hard
3849+
If \`true\`, turn the install-script policy from a silent skip into a
38493850
38503851
--dangerously-allow-all-scripts
38513852
If \`true\`, bypass the \`allowScripts\` policy entirely and run every
@@ -4293,7 +4294,7 @@ Options:
42934294
Comma-separated list of packages whose install-time lifecycle scripts
42944295
42954296
--strict-allow-scripts
4296-
If \`true\`, turn the install-script policy from a warning into a hard
4297+
If \`true\`, turn the install-script policy from a silent skip into a
42974298
42984299
--dangerously-allow-all-scripts
42994300
If \`true\`, bypass the \`allowScripts\` policy entirely and run every
@@ -4443,7 +4444,7 @@ Options:
44434444
Comma-separated list of packages whose install-time lifecycle scripts
44444445
44454446
--strict-allow-scripts
4446-
If \`true\`, turn the install-script policy from a warning into a hard
4447+
If \`true\`, turn the install-script policy from a silent skip into a
44474448
44484449
--dangerously-allow-all-scripts
44494450
If \`true\`, bypass the \`allowScripts\` policy entirely and run every
@@ -4589,7 +4590,7 @@ Options:
45894590
Comma-separated list of packages whose install-time lifecycle scripts
45904591
45914592
--strict-allow-scripts
4592-
If \`true\`, turn the install-script policy from a warning into a hard
4593+
If \`true\`, turn the install-script policy from a silent skip into a
45934594
45944595
--dangerously-allow-all-scripts
45954596
If \`true\`, bypass the \`allowScripts\` policy entirely and run every
@@ -5574,7 +5575,7 @@ Options:
55745575
Comma-separated list of packages whose install-time lifecycle scripts
55755576
55765577
--strict-allow-scripts
5577-
If \`true\`, turn the install-script policy from a warning into a hard
5578+
If \`true\`, turn the install-script policy from a silent skip into a
55785579
55795580
--dangerously-allow-all-scripts
55805581
If \`true\`, bypass the \`allowScripts\` policy entirely and run every
@@ -6392,7 +6393,7 @@ Options:
63926393
Comma-separated list of packages whose install-time lifecycle scripts
63936394
63946395
--strict-allow-scripts
6395-
If \`true\`, turn the install-script policy from a warning into a hard
6396+
If \`true\`, turn the install-script policy from a silent skip into a
63966397
63976398
--dangerously-allow-all-scripts
63986399
If \`true\`, bypass the \`allowScripts\` policy entirely and run every

test/lib/commands/approve-scripts.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ t.test('approve-scripts --pending lists unreviewed packages', async t => {
5555
})
5656
await npm.exec('approve-scripts', [])
5757
const out = joinedOutput()
58-
t.match(out, /2 packages have install scripts not yet covered/)
58+
t.match(out, /2 packages have install scripts blocked because they are not covered by allowScripts/)
5959
t.match(out, /canvas@1\.0\.0/)
6060
t.match(out, /sharp@1\.0\.0/)
6161
})
@@ -67,7 +67,7 @@ t.test('approve-scripts --pending lists unreviewed packages even with ignore-scr
6767
})
6868
await npm.exec('approve-scripts', [])
6969
const out = joinedOutput()
70-
t.match(out, /2 packages have install scripts not yet covered/)
70+
t.match(out, /2 packages have install scripts blocked because they are not covered by allowScripts/)
7171
t.match(out, /canvas@1\.0\.0/)
7272
t.match(out, /sharp@1\.0\.0/)
7373
})

test/lib/commands/edit.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const spawk = tspawk(t)
2222
const npmConfig = {
2323
config: {
2424
'ignore-scripts': false,
25+
// Phase 2 gates dependency install scripts by default. `npm edit`
26+
// rebuilds the edited package via `npm rebuild`, which honors the
27+
// allowScripts gate, so opt every script in for these tests to exercise
28+
// the editor -> rebuild -> install-script flow.
29+
'dangerously-allow-all-scripts': true,
2530
editor: 'testeditor',
2631
'script-shell': process.platform === 'win32' ? process.env.COMSPEC : 'sh',
2732
},

test/lib/commands/rebuild.js

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ t.test('completion', async t => {
222222
t.type(res, Array)
223223
})
224224

225-
t.test('emits Phase 1 advisory warning for unreviewed install scripts', async t => {
225+
t.test('emits blocked warning for unreviewed install scripts', async t => {
226226
const { npm, logs } = await setupMockNpm(t, {
227227
prefixDir: {
228228
'package.json': JSON.stringify({ name: 'host', version: '1.0.0' }),
@@ -240,7 +240,7 @@ t.test('emits Phase 1 advisory warning for unreviewed install scripts', async t
240240
await npm.exec('rebuild', [])
241241
t.match(
242242
logs.warn.byTitle('rebuild'),
243-
[/install scripts not yet covered by allowScripts/]
243+
[/install scripts blocked because they are not covered by allowScripts/]
244244
)
245245
})
246246

@@ -264,7 +264,7 @@ t.test('global advisory warning points at npm config set, not approve-scripts',
264264
})
265265
await npm.exec('rebuild', [])
266266
const warn = logs.warn.byTitle('rebuild').join('\n')
267-
t.match(warn, /install scripts not yet covered by allowScripts/)
267+
t.match(warn, /install scripts blocked because they are not covered by allowScripts/)
268268
t.match(warn, /npm config set allow-scripts=canvas/)
269269
t.notMatch(warn, /approve-scripts/)
270270
})
@@ -307,6 +307,54 @@ t.test('no advisory warning when allowScripts covers the package', async t => {
307307
t.strictSame(logs.warn.byTitle('rebuild'), [])
308308
})
309309

310+
t.test('rebuild <pkg> honors the gate for an unreviewed package', async t => {
311+
const { npm, logs, prefix: path } = await setupMockNpm(t, {
312+
prefixDir: {
313+
'package.json': JSON.stringify({
314+
name: 'host',
315+
version: '1.0.0',
316+
dependencies: { canvas: '1.0.0' },
317+
}),
318+
'package-lock.json': JSON.stringify({
319+
name: 'host',
320+
version: '1.0.0',
321+
lockfileVersion: 3,
322+
requires: true,
323+
packages: {
324+
'': { name: 'host', version: '1.0.0', dependencies: { canvas: '1.0.0' } },
325+
'node_modules/canvas': {
326+
version: '1.0.0',
327+
resolved: 'https://registry.npmjs.org/canvas/-/canvas-1.0.0.tgz',
328+
hasInstallScript: true,
329+
},
330+
},
331+
}),
332+
node_modules: {
333+
canvas: {
334+
'package.json': JSON.stringify({
335+
name: 'canvas',
336+
version: '1.0.0',
337+
scripts: {
338+
install: "node -e \"require('fs').writeFileSync('ran', '')\"",
339+
},
340+
}),
341+
},
342+
},
343+
},
344+
})
345+
346+
const ranFile = resolve(path, 'node_modules/canvas/ran')
347+
t.throws(() => fs.statSync(ranFile))
348+
349+
await npm.exec('rebuild', ['canvas'])
350+
351+
t.throws(() => fs.statSync(ranFile), 'unreviewed install script must not run')
352+
t.match(
353+
logs.warn.byTitle('rebuild'),
354+
[/install scripts blocked because they are not covered by allowScripts/]
355+
)
356+
})
357+
310358
t.test('rebuild <name> never targets a bundled dependency', async t => {
311359
const { npm, prefix: path } = await setupMockNpm(t, {
312360
prefixDir: {

0 commit comments

Comments
 (0)