Skip to content

fix: recognize allowScripts for local link targets#9490

Open
cyphercodes wants to merge 2 commits into
npm:latestfrom
cyphercodes:fix/9488-allow-scripts-file-deps
Open

fix: recognize allowScripts for local link targets#9490
cyphercodes wants to merge 2 commits into
npm:latestfrom
cyphercodes:fix/9488-allow-scripts-file-deps

Conversation

@cyphercodes
Copy link
Copy Markdown
Contributor

Summary

  • Recognize local directory link targets by their incoming link source when matching allowScripts policy entries.
  • Reuse that source identity when approve-scripts/deny-scripts derive file dependency policy keys.
  • Add coverage for reviewed local file: dependency link targets.

Fixes #9488

Testing

  • node node_modules/tap/bin/run.js --no-coverage workspaces/arborist/test/script-allowed.js workspaces/arborist/test/unreviewed-scripts.js test/lib/utils/allow-scripts-writer.js test/lib/utils/check-allow-scripts.js test/lib/utils/resolve-allow-scripts.js
  • node node_modules/eslint/bin/eslint.js lib/utils/allow-scripts-writer.js test/lib/utils/allow-scripts-writer.js workspaces/arborist/lib/script-allowed.js workspaces/arborist/test/script-allowed.js workspaces/arborist/test/unreviewed-scripts.js
  • git diff --check
  • Manual repro: local file: dependency with allowScripts: { "file:../testdep": false } no longer emits an allow-scripts warning; npm approve-scripts --all writes file:../testdep.

@cyphercodes cyphercodes requested review from a team as code owners June 4, 2026 15:58
@owlstronaut
Copy link
Copy Markdown
Contributor

@JamieMagee So this looks valid to me for v11, if the user has added file: as an approve script we shouldn't warn.

One thing I'd like your read on for v12 before this lands there: in rebuild.js the scriptsAllowed check short-circuits on node.isLink, with the comment "Bypasses: --dangerously-allow-all-scripts, links and workspaces (the owner is responsible)." I confirmed that means "file:../dep": false doesn't actually block the script, it runs in all cases (true, false, absent) on latest. Before this PR you'd at least see a warning. With it merged the warning is gone too, so the user gets total silence on top of a script that still runs. Is the isLink bypass the intended v12 behavior (file: deps treated like workspaces, owner-managed, allowScripts entries are warning-control only), or a gap that should be closed before v12 ships?

Copy link
Copy Markdown
Contributor

@owlstronaut owlstronaut left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds node.realpath and node.path to the spec list for every node. A registry-resolved package can't match a file policy key because matchFileOrDir short circuits. With this change a registry package like sharp@0.33.0 installed at proj/node_modules/sharp will match a k ey like file:/proj/node_modules/sharp": true.

Comment on lines +124 to +125
add(node.realpath)
add(node.path)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to @owlstronaut. The guard on line 113 is always true since linksIn is always a Set, so these two lines add realpath/path for every node, not just link targets. A registry package's install path then becomes a match candidate for file:/directory keys. I checked locally: a registry node returns true for {"file:node_modules/<name>": true} where base returns null.

Can we gate the paths on the real link-target case, e.g. only add them when !node.resolved && node.linksIn.size > 0?

// Denying always writes `"<name>": false`, regardless of `--allow-scripts-pin`, per the
// RFC's asymmetric-pin rule.

const primaryResolvedSource = (node) => resolvedSourceSpecs(node)[0] || ''
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same root cause leaks into the writer. If a link target has an empty linksIn (so resolved is null), resolvedSourceSpecs(node)[0] is node.realpath, an absolute path. versionedKeyFor/nameKeyFor then take the startsWith('/') branch and write something like /proj/node_modules/A into allowScripts instead of file:../A. Better to prefer a file: candidate over a bare path, or return null so we don't persist a machine-specific key. Fixing comment 1 with the size > 0 guard mostly covers this too.

t.end()
})

t.test('file path — link target matches incoming link source', t => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a negative case here? A registry node (resolved = tarball URL, empty linksIn) shouldn't match a file: key by its install path: t.equal(isScriptAllowed(reg, { 'file:node_modules/<name>': true }), null). That pins down the boundary @owlstronaut flagged. Covering an empty linksIn Set and a link with resolved === null would help too.

@JamieMagee
Copy link
Copy Markdown
Contributor

One thing I'd like your read on for v12 before this lands there: in rebuild.js the scriptsAllowed check short-circuits on node.isLink, with the comment "Bypasses: --dangerously-allow-all-scripts, links and workspaces (the owner is responsible)." I confirmed that means "file:../dep": false doesn't actually block the script, it runs in all cases (true, false, absent) on latest. Before this PR you'd at least see a warning. With it merged the warning is gone too, so the user gets total silence on top of a script that still runs. Is the isLink bypass the intended v12 behavior (file: deps treated like workspaces, owner-managed, allowScripts entries are warning-control only), or a gap that should be closed before v12 ships?

The isLink bypass is the part I'm not sure we want once v12 flips the default to block. An explicit false that still runs, silently now, is a footgun. The bypass already has a separate node.isWorkspace clause, so I'd drop the blanket node.isLink and let non-workspace file:/link: targets fall through to isScriptAllowed. The Link's own resolved is file:../dep, so it matches there without any of the linksIn machinery. Workspaces keep their owner-managed bypass and external file: deps actually get enforced.

@cyphercodes
Copy link
Copy Markdown
Contributor Author

Updated this PR to address the review feedback around link-target matching.

Changes:

  • Only adds incoming link specs / real target paths for actual unresolved link targets with incoming links.
  • Keeps registry nodes from matching file: policy keys by their install path.
  • Added regression coverage for registry install-path matching and empty linksIn sets, plus writer behavior for empty link targets.

Local verification:

  • node ./scripts/resetdeps.js
  • node . exec -- tap workspaces/arborist/test/script-allowed.js test/lib/utils/allow-scripts-writer.js
  • node . exec -- tap workspaces/arborist/test/unreviewed-scripts.js
  • node . run lint -- workspaces/arborist/lib/script-allowed.js workspaces/arborist/test/script-allowed.js test/lib/utils/allow-scripts-writer.js
  • git diff --check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] npm i shows allow-scripts uncovered dependency warning even if dependency was already approved

3 participants