File Explorer

/proc/self/root/proc/1/task/1/root/node24/lib/node_modules/npm/lib/utils

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

allow-scripts-writer.js10.6 KB · 324 lines
const npa = require('npm-package-arg')const { log } = require('proc-log')const { getTrustedRegistryIdentity } = require('@npmcli/arborist/lib/script-allowed.js') // Pure helpers that implement the RFC's pin-mismatch table for// `npm approve-scripts` and `npm deny-scripts`.//// Approving writes either `"<spec>": true` or `"<name>": true` to the// project's `allowScripts` field, depending on `--allow-scripts-pin` and the currently// installed versions.//// Denying always writes `"<name>": false`, regardless of `--allow-scripts-pin`, per the// RFC's asymmetric-pin rule. // Convert an arborist Node into the spec string used for a versioned policy// entry. Returns `null` if the node cannot be represented as a versioned key// derived from trusted sources (lockfile URL for registry, hosted shortcut// for git, the resolved file path for local installs). Never falls back to// `node.packageName` / `node.version`, which are tarball-controlled.const versionedKeyFor = (node) => {  if (!node) {    return null  }  /* istanbul ignore next: callers guarantee a string resolved */  const resolved = typeof node.resolved === 'string' ? node.resolved : ''  if (resolved.startsWith('git')) {    try {      const parsed = npa(resolved)      if (parsed.hosted) {        const committish = parsed.gitCommittish || parsed.hosted.committish        const base = parsed.hosted.shortcut({ noCommittish: true })        return committish ? `${base}#${committish}` : base      }    } catch {      /* istanbul ignore next: npa already parsed this string in keyTargetsNode */      return null    }    return null  }  if (/^https?:\/\//.test(resolved)) {    const trusted = getTrustedRegistryIdentity(node)    if (trusted && trusted.version) {      return `${trusted.name}@${trusted.version}`    }    // Registry node with a resolved URL that versionFromTgz couldn't    // parse (private-registry mirror, alternate CDN URL shape). Leave a    // breadcrumb so users notice when policy keys are silently pruned.    log.silly(      'allow-scripts',      `unable to derive trusted versioned key for ${node.path || node.name || '<unknown>'} ` +      `(resolved: ${resolved}); key will be pruned on next save`    )    return null  }  /* istanbul ignore next: 'file:' and '/' branches are each covered separately */  if (resolved.startsWith('file:') || resolved.startsWith('/')) {    return resolved  }  // No trusted source. Refuse to compose a key from attacker-controlled  // `node.packageName` / `node.version`.  /* istanbul ignore next: callers filter out non-registry/non-file nodes before reaching this fallback */  return null} // Convert an arborist Node into the spec string used for a name-only policy// entry. Same trust rules as versionedKeyFor — returns `null` rather than// falling back to tarball-controlled fields.const nameKeyFor = (node) => {  if (!node) {    return null  }  /* istanbul ignore next: callers guarantee a string resolved */  const resolved = typeof node.resolved === 'string' ? node.resolved : ''  if (resolved.startsWith('git')) {    try {      const parsed = npa(resolved)      if (parsed.hosted) {        return parsed.hosted.shortcut({ noCommittish: true })      }    } catch {      /* istanbul ignore next: npa already parsed this string in keyTargetsNode */      return null    }    return null  }  if (resolved.startsWith('file:') || resolved.startsWith('/')) {    return resolved  }  // Registry deps: only the URL-derived (or edges-derived, in the  // omit-lockfile case) trusted name is acceptable.  const trusted = getTrustedRegistryIdentity(node)  return trusted ? trusted.name : null} const isSingleVersionPin = (key) => {  try {    const parsed = npa(key)    return parsed.type === 'version'  } catch {    return false  }} // Build the warning string emitted when an existing deny entry blocks// an approval. Per RFC, a name-only deny ("pkg": false) is widest and// the only remediation is to remove the entry. A versioned deny// ("pkg@1.2.3": false or a disjunction) blocks only specific versions;// the user can either widen it via `npm deny-scripts <name>` or remove// it to approve the currently-installed version only.const denyWarning = (key, subject, name) => {  if (isNameOnlyKey(key)) {    return `${key} is denied; remove the entry from allowScripts to approve ${subject}.`  }  /* istanbul ignore next: name fallback is defensive; callers pass nameKeyFor(sample) */  const widenTarget = name || 'this package'  return `${key} is a versioned deny; run \`npm deny-scripts ${widenTarget}\` ` +    `to widen the deny to all versions of ${widenTarget}, or remove the entry ` +    `to approve ${subject}.`} const isNameOnlyKey = (key) => {  try {    const parsed = npa(key)    if (parsed.type === 'tag') {      return true    }    if (parsed.type === 'range') {      return parsed.fetchSpec === '*'        || parsed.rawSpec === ''        || parsed.rawSpec === '*'    }    return false  } catch {    /* istanbul ignore next: keys reaching this helper have already parsed via keyTargetsNode */    return false  }} // Does this policy key target this node by identity (ignoring the// allow/deny value)?//// Registry keys (`tag`, `range`, `version`) require a trusted identity on// the node. If the node has no `getTrustedRegistryIdentity` result, the// key does not match — never fall back to `node.name`, which is the// install-directory name and is forgeable through aliases / manifest// confusion.const keyTargetsNode = (key, node) => {  let parsed  try {    parsed = npa(key)  } catch {    return false  }  switch (parsed.type) {    case 'tag':    case 'range':    case 'version': {      const trusted = getTrustedRegistryIdentity(node)      if (!trusted) {        return false      }      return trusted.name === parsed.name    }    case 'git': {      let resolvedParsed      try {        resolvedParsed = node.resolved ? npa(node.resolved) : null      } catch {        /* istanbul ignore next */        return false      }      const keyHost = parsed.hosted?.ssh({ noCommittish: true })      const nodeHost = resolvedParsed?.hosted?.ssh({ noCommittish: true })      return !!(keyHost && nodeHost && keyHost === nodeHost)    }    case 'file':    case 'directory':    case 'remote':      return node.resolved === parsed.saveSpec || node.resolved === parsed.fetchSpec    default:      return false  }} // Apply approvals for all currently-installed versions of a single package.//// `nodes` must all share an identity (same package name for registry deps,// or same hosted shortcut for git deps, etc.). The caller is responsible// for grouping nodes correctly.//// Returns `{ allowScripts, changes, warning }` where://   - `allowScripts` is the new object (the input is never mutated)//   - `changes` is a list of `{ key, change }` entries describing edits//   - `warning` is an optional message to surface to the userconst applyApprovalForPackage = (existing, nodes, { pin = true } = {}) => {  const allowScripts = { ...existing }  const changes = []   if (!Array.isArray(nodes) || nodes.length === 0) {    return { allowScripts, changes }  }   const sample = nodes[0]  const name = nameKeyFor(sample)   // Deny-wins: any existing false that targets any installed version aborts.  for (const node of nodes) {    for (const [key, value] of Object.entries(allowScripts)) {      if (value === false && keyTargetsNode(key, node)) {        /* istanbul ignore next: name fallback covers the empty-name edge case */        const subject = name || 'this package'        return {          allowScripts,          changes,          warning: denyWarning(key, subject, name),        }      }    }  }   if (!pin) {    // Name-only mode: collapse any single-version pins for this package    // into a single name-only entry.    for (const key of Object.keys(allowScripts)) {      if (        keyTargetsNode(key, sample) &&        key !== name &&        isSingleVersionPin(key) &&        allowScripts[key] === true      ) {        delete allowScripts[key]      }    }     /* istanbul ignore else: name === null is the no-identity path tested separately */    if (name && allowScripts[name] !== true) {      allowScripts[name] = true      changes.push({ key: name, change: 'added' })    }    return { allowScripts, changes }  }   // Pin mode. For each currently installed version, write a single-version  // pin if one is not already in place. Stale single-version pins for this  // package are removed. Per the RFC's pin-mismatch table, an existing  // name-only entry (`pkg: true`) is replaced by `pkg@x.y.z: true` once  // every installed version has a pin.  const installedKeys = new Set(nodes.map(versionedKeyFor).filter(Boolean))   for (const key of Object.keys(allowScripts)) {    if (      keyTargetsNode(key, sample) &&      isSingleVersionPin(key) &&      allowScripts[key] === true &&      !installedKeys.has(key)    ) {      delete allowScripts[key]      changes.push({ key, change: 'removed-stale' })    }  }   for (const key of installedKeys) {    if (allowScripts[key] !== true) {      allowScripts[key] = true      changes.push({ key, change: 'added' })    }  }   // Upgrade: drop the name-only entry once every installed version has a  // pin. The operation is convergent: running the command twice produces  // the same shape regardless of the starting state.  if (    installedKeys.size > 0 &&    name &&    !installedKeys.has(name) &&    allowScripts[name] === true  ) {    delete allowScripts[name]    changes.push({ key: name, change: 'replaced-by-pin' })  }   return { allowScripts, changes }} // Apply a deny for a single package. Always name-only; ignores `--allow-scripts-pin`.const applyDenyForPackage = (existing, nodes) => {  const allowScripts = { ...existing }  const changes = []   if (!Array.isArray(nodes) || nodes.length === 0) {    return { allowScripts, changes }  }   const sample = nodes[0]  const name = nameKeyFor(sample)  if (!name) {    return { allowScripts, changes }  }   // Drop any pinned allow entries for this package: the name-only deny  // overrides them anyway, and leaving them in place is confusing.  for (const key of Object.keys(allowScripts)) {    if (keyTargetsNode(key, sample) && key !== name) {      delete allowScripts[key]      changes.push({ key, change: 'removed-pinned-allow' })    }  }   if (allowScripts[name] !== false) {    allowScripts[name] = false    changes.push({ key: name, change: 'added' })  }  return { allowScripts, changes }} module.exports = {  applyApprovalForPackage,  applyDenyForPackage,  versionedKeyFor,  nameKeyFor,  keyTargetsNode,  isSingleVersionPin,}