Skip to content

pnpm workspace importers add phantom components with the semver specifier as version #339

Closed

Description

@TSP0wni3

Summary

On pnpm workspaces, hooray scan project adds phantom inventory components whose version is the declared semver specifier (for example ^1.0.0) instead of the resolved version. Those phantom components produce false OSV matches for old advisories that the resolved version does not have.

Classification

Bug

Version and provenance

  • Hooray: 0.9.0
  • Linux x64 release asset SHA-256: c1bc87e4a2382d957066b01d15d12543025776f1272266ff3e9047bdad55c923
  • Platform: Linux x86_64
  • Follow-up to fix(parsers): accept multi-document pnpm-lock.yaml #337 (multi-document pnpm-lock.yaml, fixed in 0.9.0)
  • The real-world application source is private; the generic fixture below reproduces the defect without it

Reproduction

Create a workspace with pnpm-workspace.yaml:

packages:
  - 'apps/*'

Root package.json:

{
  "name": "probe",
  "private": true,
  "dependencies": {
    "is-odd": "^3.0.1"
  }
}

apps/web/package.json:

{
  "name": "web",
  "private": true,
  "dependencies": {
    "is-even": "^1.0.0"
  }
}

Two-document pnpm-lock.yaml (the shape pnpm 12 writes) with both importer entries and resolved packages for is-odd@3.0.1 and is-even@1.0.0.

Run:

hooray scan project . --policy hooray-policy.yaml --offline --format json

Expected and actual behavior

Expected: the inventory contains exactly the resolved components: is-odd@3.0.1 and is-even@1.0.0.

Actual: the inventory additionally contains is-even with the literal version ^1.0.0:

[
  { "name": "is-even", "version": "1.0.0" },
  { "name": "is-even", "version": "^1.0.0" },
  { "name": "is-odd", "version": "3.0.1" }
]

On a real workspace, every such specifier-versioned phantom matched historical advisories, inflating ten true vulnerability findings into 83, including CVEs fixed years before the resolved release (for example a package resolved to 19.2.8 flagged with a 2018 advisory via its ^19.2.6 phantom).

Evidence and scope

The minimal run is offline and dependency-free. The phantom appears only for dependencies of workspace importers (apps/web), not for the root importer. A single-document lockfile with the same content does not show the phantom in earlier releases because the scan aborted outright (#337).

This issue covers inventory construction from workspace importers; OSV matching against the phantom versions is the visible consequence.

Acceptance criteria

  • The fixture inventory contains only resolved versions; no component carries a range specifier as its version.
  • OSV matching runs against resolved versions only.
  • The real-world scan described above returns to the ten true findings shared with the reference scanner.
  • Regression tests cover workspace importers in two-document pnpm 12 lockfiles.

Activity

  1. wakemeup0 commented on Sep 22, 2026

    @wakemeup0
    Contributor

    This was generated by AI during triage.

    Agent brief

    Behavior and contract:

    • pnpm 12 writes workspace-importer dependencies in pnpm-lock.yaml as plain specifier strings (is-even: ^1.0.0) while the root importer keeps {specifier, version} objects. pnpm_resolved_str (src/parsers/pnpm.rs:302-326) falls through to Some((dep, text)) for any bare string without @/://, so the specifier becomes a component version: phantom is-even@^1.0.0 alongside the resolved is-even@1.0.0 from packages:.
    • Phantom components inflate OSV results (reporter: 10 true findings → 83).

    Invariants:

    • Inventory components carry resolved versions only; specifier text is never a version.
    • Importer-only dependencies absent from packages: remain supported for concrete versions (src/parsers/pnpm.rs:103-108).
    • link:/workspace:/file:/portal:/tarball: specifiers still produce no component.
    • Scope classification via importer-field reachability is preserved: a specifier-valued dep must still register its resolved package as a root under the correct importer field.

    Acceptance criteria:

    • The fixture inventory contains only resolved versions; no component carries a range specifier as its version.
    • OSV matching runs against resolved versions only.
    • Regression tests cover workspace importers in two-document pnpm 12 lockfiles.

    Non-goals:

    • No semver-range solving; resolution uses the lockfile's own packages:/snapshots: entries (first recorded version, matching resolve_lock_component fallback semantics).
    • No change to edge resolution (pnpm_dep_target already falls back to name lookup).

    Evidence and exact reproduction:

    • Defect seam: src/parsers/pnpm.rs:322-325 (bare-string fallthrough) at base 7f706fe.
    • Reproduction: two-document pnpm-lock.yaml — doc 1 importers: with root . object deps and apps/web plain-string specifier deps; doc 2 settings/packages/snapshots. hooray scan project . --offline --format json yields is-even@^1.0.0 phantom.
    • Prior related fix: 191e043 (pnpm specifiers become phantom components (npm: alias, link:, tarball) #44) covered npm:/link:/tarball phantoms but not bare range specifiers.

    Risks and blockers:

    • Bare strings that are concrete versions (pnpm <5 form) must keep working; tag specifiers (latest) and wildcards (1.x) should resolve by name, not become versions.
    • Unresolvable specifier (name absent from packages:) yields no component rather than a specifier-versioned one.
  2. added
    ready-for-agentEvidence, scope, and acceptance criteria are ready for agent implementation
    false-positiveConfirmed finding reported where the input is clean
    ecosystem:npmnpm/pnpm/Yarn dependency scope
    area:scaDependency inventory, OSV vulnerabilities, or remediation
    and removed
    needs-triageEvidence and scope still need triage
    on Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:scaDependency inventory, OSV vulnerabilities, or remediationbugSomething isn't workingecosystem:npmnpm/pnpm/Yarn dependency scopefalse-positiveConfirmed finding reported where the input is cleanpriority:highHigh user or data impactready-for-agentEvidence, scope, and acceptance criteria are ready for agent implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions