Skip to content

fix: require integrity for tarball-shaped lockfile resolutions#11966

Merged
zkochan merged 5 commits into
mainfrom
fix-vuln5
May 26, 2026
Merged

fix: require integrity for tarball-shaped lockfile resolutions#11966
zkochan merged 5 commits into
mainfrom
fix-vuln5

Conversation

@zkochan

@zkochan zkochan commented May 26, 2026

Copy link
Copy Markdown
Member

Summary

A tampered pnpm-lock.yaml that strips the integrity field from a tarball resolution let the worker download the URL contents and mint a fresh integrity from the unverified bytes (worker/src/start.ts:189-204: if (integrity) skipped verification when absent, then integrity: integrity ?? calcIntegrity(buffer) stored whatever it got). An attacker who could both alter the lockfile (e.g. via a pull request that drops integrity:) and serve modified content at the referenced tarball URL could install a tampered package without any error — including under --frozen-lockfile.

pkgSnapshotToResolution now fails closed at lockfile-read time with ERR_PNPM_MISSING_TARBALL_INTEGRITY whenever a tarball-shaped resolution (no type field — covers plain remote, registry-derived, file:, and gitHosted entries) lacks integrity. This matches pacquet's existing pacquet_package_manager::missing_tarball_integrity guard at install_package_by_snapshot.rs:113-114.

The fix sits at the lockfile-read chokepoint every install path flows through (deps-resolver, deps-restorer, graph-builder), so both isolated and hoisted node-linkers are covered. The worker's own if (integrity) mint path is intentionally left in place because it is still the legitimate fresh-install code path for resolutions that have not yet been recorded in a lockfile (the resolver attaches the computed integrity back to the resolution at packageRequester.ts:297-298 before the lockfile write).

I scanned every pnpm-lock.yaml fixture in the repo for tarball entries without integrity — there are none, so no existing test breaks from the stricter check.

Pacquet's structural guard for the same condition already existed at both the canonical install-side site (tarball_url_and_integrity) and the upfront warm-batch short-circuit (snapshot_cache_key), but had no negative test. Added one against snapshot_cache_key that constructs a LockfileResolution::Tarball { integrity: None } — the exact shape a tampered lockfile produces — and asserts the orchestrator surfaces InstallPackageBySnapshotError::MissingTarballIntegrity upfront, before the rayon batch runs.

Credit to AutoFyn for finding and reporting the issue.

Test plan

  • New unit test in lockfile/utils/test/pkgSnapshotToResolution.ts asserts the throw fires for plain remote, git-hosted, and file: tarball entries that lack integrity.
  • The existing cdn.sheetjs.com test case is updated to carry integrity (it now represents the realistic lockfile state).
  • New snapshot_cache_key_rejects_tarball_without_integrity test in pacquet/crates/package-manager/src/create_virtual_store/tests.rs covers the equivalent guard on the pacquet side.
  • pnpm --filter @pnpm/lockfile.utils run lint clean.
  • pnpm --filter @pnpm/lockfile.utils test pkgSnapshotToResolution.ts passes.
  • cargo nextest run -p pacquet-package-manager snapshot_cache_key — all three (including the new one) pass.
  • cargo clippy --locked -p pacquet-package-manager --all-targets -- --deny warnings clean.

Written by an agent (Claude Code, claude-opus-4-7).

Summary by CodeRabbit

  • Bug Fixes

    • Lockfile parsing now rejects remote tarball entries that lack an integrity hash, preventing installation of unverified package content (while still allowing known git-hosted and local file tarballs).
  • Tests

    • Added tests ensuring missing integrity causes failures and that git-hosted/file tarballs remain exempt.
  • Documentation

    • Updated release notes and changeset text describing the stricter lockfile integrity behavior.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds lockfile parsing validation rejecting tarball-shaped resolutions missing an integrity field (ERR_PNPM_MISSING_TARBALL_INTEGRITY); exports git-host URL helper; updates/expands JS tests, adds a Rust test in pacquet, and includes a changeset documenting the behavior.

Changes

Tarball Integrity Validation

Layer / File(s) Summary
Tarball integrity validation and tests
lockfile/utils/src/pkgSnapshotToResolution.ts, lockfile/utils/src/toLockfileResolution.ts, lockfile/utils/test/pkgSnapshotToResolution.ts
Import PnpmError, export isGitHostedTarballUrl, and enforce that tarball-shaped lockfile resolution entries include an integrity field; throw MISSING_TARBALL_INTEGRITY when missing. Update an existing tarball expectation and add Jest tests asserting rejection for registry/CDN tarballs and allowing git-host/file: exemptions.
Pacquet snapshot_cache_key test
pacquet/crates/package-manager/src/create_virtual_store/tests.rs
Add test and helper constructing PackageMetadata with LockfileResolution::Tarball { integrity: None } and assert snapshot_cache_key fails with MissingTarballIntegrity wrapped in CreateVirtualStoreError::InstallPackageBySnapshot for the expected package key.
Security behavior changeset
.changeset/require-tarball-integrity.md
Document the patch-level lockfile parsing validation that rejects tarball resolution entries lacking integrity and note the ERR_PNPM_MISSING_TARBALL_INTEGRITY failure mode and listed exemptions.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly Related PRs

  • pnpm/pnpm#11509: Touches toLockfileResolution tarball/gitHosted handling; code-level relation to git-host detection changes in this PR.
  • pnpm/pnpm#11773: Introduces tarball resolver behavior producing integrity: None outputs; directly related to the new integrity validation guard.
  • pnpm/pnpm#11481: Changes git-hosted tarball fetching/writing to pin integrity, which affects when the new missing-integrity guard triggers.

Poem

🐰 I nibble bytes and follow clues,
A tarball's truth I always choose.
If integrity is gone from sight,
I stamp my paw and stop the flight.
🥕🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main security fix: requiring the integrity field for tarball-shaped lockfile resolutions, which is the core change across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-vuln5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Require integrity field for tarball-shaped lockfile resolutions

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Reject tarball resolutions missing integrity field at lockfile-read time
• Prevents attacker from installing tampered packages via lockfile tampering
• Adds ERR_PNPM_MISSING_TARBALL_INTEGRITY error for all tarball types
• Covers plain remote, registry, file:, and git-hosted tarball entries
Diagram
flowchart LR
  A["Lockfile Read"] --> B["pkgSnapshotToResolution"]
  B --> C{"Tarball-shaped<br/>resolution?"}
  C -->|Yes| D{"Has integrity<br/>field?"}
  D -->|No| E["Throw ERR_PNPM_MISSING_TARBALL_INTEGRITY"]
  D -->|Yes| F["Return resolution"]
  C -->|No| F
  E --> G["Installation blocked"]
  F --> H["Safe installation"]

Loading

Grey Divider

File Changes

1. lockfile/utils/src/pkgSnapshotToResolution.ts 🐞 Bug fix +18/-3

Add integrity validation for tarball resolutions

• Added import of PnpmError for error handling
• Added validation check that rejects tarball-shaped resolutions without integrity field
• Throws ERR_PNPM_MISSING_TARBALL_INTEGRITY with descriptive error message and recovery hint
• Refactored resolution variable assignment for cleaner code

lockfile/utils/src/pkgSnapshotToResolution.ts


2. lockfile/utils/test/pkgSnapshotToResolution.ts 🧪 Tests +35/-0

Add tests for missing tarball integrity validation

• Updated existing cdn.sheetjs.com test case to include integrity field
• Added comprehensive test suite for missing integrity validation
• Tests cover plain remote, empty resolution, git-hosted, and file: tarball types
• Verifies that all tarball-shaped resolutions without integrity throw the correct error

lockfile/utils/test/pkgSnapshotToResolution.ts


3. .changeset/require-tarball-integrity.md 📝 Documentation +6/-0

Add changeset for tarball integrity requirement

• Documents the security fix in changeset format
• Marks changes as patch version for @pnpm/lockfile.utils and pnpm
• Explains the vulnerability and the new error behavior
• References pacquet's equivalent security guard

.changeset/require-tarball-integrity.md


Grey Divider

Qodo Logo

zkochan added a commit that referenced this pull request May 26, 2026
…n in snapshot_cache_key

Match the upstream guard landed alongside #11966
(`lockfile/utils/src/pkgSnapshotToResolution.ts`) with a test on
the pacquet side: a `LockfileResolution::Tarball` with `integrity:
None` — what a tampered lockfile looks like — must short-circuit
the warm-batch cache-key derivation by surfacing
`InstallPackageBySnapshotError::MissingTarballIntegrity`. The
structural guard already existed but had no negative test.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pacquet/crates/package-manager/src/create_virtual_store/tests.rs (1)

258-267: ⚡ Quick win

Trim the test doc comment to the contract-level intent only.

This block duplicates behavioral detail already captured by the test body; keep only brief why/contract context.

As per coding guidelines, "Tests are documentation. Do not duplicate them in prose." and "Doc comments ... are not a re-narration of the body."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pacquet/crates/package-manager/src/create_virtual_store/tests.rs` around
lines 258 - 267, The test doc comment above the test for
snapshot_cache_key/MissingTarballIntegrity is overly detailed; trim it to a
single-sentence contract-level intent describing that a Tarball resolution
missing integrity must be rejected (e.g., "Ensure snapshot_cache_key returns
MissingTarballIntegrity for resolutions missing integrity"), remove the
implementation/rationale history and upstream references (pnpm/pnpm#11966) so
the comment only states the expected behavior and context.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pacquet/crates/package-manager/src/create_virtual_store/tests.rs`:
- Around line 258-267: The test doc comment above the test for
snapshot_cache_key/MissingTarballIntegrity is overly detailed; trim it to a
single-sentence contract-level intent describing that a Tarball resolution
missing integrity must be rejected (e.g., "Ensure snapshot_cache_key returns
MissingTarballIntegrity for resolutions missing integrity"), remove the
implementation/rationale history and upstream references (pnpm/pnpm#11966) so
the comment only states the expected behavior and context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d82f97d6-aff0-4150-9460-bf619729fdea

📥 Commits

Reviewing files that changed from the base of the PR and between 5215cb2 and e3321b1.

📒 Files selected for processing (1)
  • pacquet/crates/package-manager/src/create_virtual_store/tests.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/package-manager/src/create_virtual_store/tests.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/package-manager/src/create_virtual_store/tests.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/package-manager/src/create_virtual_store/tests.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/package-manager/src/create_virtual_store/tests.rs
🔇 Additional comments (1)
pacquet/crates/package-manager/src/create_virtual_store/tests.rs (1)

2-3: LGTM!

Also applies to: 269-305

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01      7.9±0.21ms   551.5 KB/sec    1.00      7.8±0.15ms   559.3 KB/sec

@codecov-commenter

codecov-commenter commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.95%. Comparing base (f578281) to head (1fb76af).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11966      +/-   ##
==========================================
+ Coverage   87.93%   87.95%   +0.01%     
==========================================
  Files         228      228              
  Lines       27810    27819       +9     
==========================================
+ Hits        24456    24469      +13     
+ Misses       3354     3350       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.925 ± 0.053 1.846 2.022 1.01 ± 0.04
pacquet@main 1.906 ± 0.044 1.841 1.984 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.9245815834800002,
      "stddev": 0.0527983981835926,
      "median": 1.9144956128800001,
      "user": 2.7676179000000003,
      "system": 3.2526983199999995,
      "min": 1.8463744633799999,
      "max": 2.0220719483800003,
      "times": [
        1.94003576238,
        1.90310172338,
        1.9095198263800002,
        1.8463744633799999,
        1.98045952638,
        1.8623207613800001,
        2.0220719483800003,
        1.9045097773800002,
        1.95795064638,
        1.91947139938
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.90589989538,
      "stddev": 0.043962395887157724,
      "median": 1.89668446438,
      "user": 2.7572688,
      "system": 3.213904219999999,
      "min": 1.84148682738,
      "max": 1.98381722638,
      "times": [
        1.94668986638,
        1.87584375638,
        1.84148682738,
        1.90018175038,
        1.9306281333800002,
        1.9406379813799999,
        1.85803101438,
        1.88849521938,
        1.98381722638,
        1.89318717838
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 639.2 ± 33.5 613.3 730.6 1.00
pacquet@main 640.5 ± 10.8 628.9 667.0 1.00 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6391949954999999,
      "stddev": 0.03354166546545221,
      "median": 0.6275619306000001,
      "user": 0.35133523999999994,
      "system": 1.3478787999999997,
      "min": 0.6132971591,
      "max": 0.7306369511,
      "times": [
        0.7306369511,
        0.6232349901,
        0.6256207991000001,
        0.6275941581000001,
        0.6254678531000001,
        0.6275297031000001,
        0.6329519091000001,
        0.6508673841000001,
        0.6347490481000001,
        0.6132971591
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.640504841,
      "stddev": 0.01080651134339348,
      "median": 0.6378664426,
      "user": 0.36111313999999994,
      "system": 1.3567674999999997,
      "min": 0.6288607891000001,
      "max": 0.6669768101000001,
      "times": [
        0.6669768101000001,
        0.6418517551,
        0.6398801531000001,
        0.6350400031000001,
        0.6288607891000001,
        0.6343026451000001,
        0.6330376001000001,
        0.6358527321,
        0.6486739711000001,
        0.6405719511000001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.272 ± 0.061 2.210 2.415 1.03 ± 0.03
pacquet@main 2.208 ± 0.036 2.162 2.261 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.2720905974999996,
      "stddev": 0.06066994237659803,
      "median": 2.2582653174000002,
      "user": 3.85872264,
      "system": 3.0269531599999993,
      "min": 2.2102259318999997,
      "max": 2.4153205419,
      "times": [
        2.2198657299,
        2.4153205419,
        2.2402636249,
        2.3362212599,
        2.2509791059,
        2.2616740309,
        2.2663663589,
        2.2548566039,
        2.2651327868999998,
        2.2102259318999997
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.2077301376999996,
      "stddev": 0.03563975784746927,
      "median": 2.2125300158999996,
      "user": 3.8479572399999995,
      "system": 3.0026020599999996,
      "min": 2.1615463969,
      "max": 2.2613509999,
      "times": [
        2.1646443558999997,
        2.2028921039,
        2.2201200438999997,
        2.2120889888999997,
        2.2195473938999997,
        2.2129710429,
        2.2567967759,
        2.1653432748999997,
        2.2613509999,
        2.1615463969
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.431 ± 0.016 1.408 1.456 1.00
pacquet@main 1.435 ± 0.059 1.391 1.596 1.00 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.43125309546,
      "stddev": 0.01581410854166845,
      "median": 1.43156360036,
      "user": 1.69980844,
      "system": 1.8585918199999998,
      "min": 1.4083581453600003,
      "max": 1.4562338883600001,
      "times": [
        1.4358264803600003,
        1.4533736483600002,
        1.4090904963600002,
        1.4292671063600002,
        1.4255496123600002,
        1.4358098383600002,
        1.4083581453600003,
        1.4338600943600002,
        1.4251616443600001,
        1.4562338883600001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.43487602646,
      "stddev": 0.059168490401430705,
      "median": 1.41619060936,
      "user": 1.7072929399999999,
      "system": 1.8171071199999997,
      "min": 1.39115108836,
      "max": 1.5955969543600002,
      "times": [
        1.4134406413600002,
        1.43877908636,
        1.4092668943600002,
        1.45186349036,
        1.5955969543600002,
        1.4212932873600002,
        1.39115108836,
        1.4005222273600002,
        1.4189405773600001,
        1.4079060173600002
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11966
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
2,272.09 ms
(-15.23%)Baseline: 2,680.22 ms
3,216.27 ms
(70.64%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,431.25 ms
(-21.03%)Baseline: 1,812.37 ms
2,174.84 ms
(65.81%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
1,924.58 ms
(-6.23%)Baseline: 2,052.38 ms
2,462.85 ms
(78.14%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
639.19 ms
(-2.65%)Baseline: 656.60 ms
787.92 ms
(81.12%)
🐰 View full continuous benchmarking report in Bencher

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lockfile/utils/src/toLockfileResolution.ts (1)

77-83: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten git-hosted URL detection to commit-pinned patterns only.

The current predicate (known host + contains tar.gz) is too permissive and can classify mutable git-host tarballs (e.g., branch/tag archives) as exempt from integrity, weakening the fail-closed guarantee.

Use a stricter matcher that only accepts commit-pinned URL shapes (or remove URL fallback and rely solely on explicit gitHosted: true when safely produced upstream).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lockfile/utils/src/toLockfileResolution.ts` around lines 77 - 83, The
isGitHostedTarballUrl predicate is too permissive (host + 'tar.gz') and must be
tightened to only accept commit-pinned tarball shapes or be removed in favor of
an explicit gitHosted flag; update the isGitHostedTarballUrl function to match
commit-specific URL patterns (e.g., codeload.github.com/.../tar.gz/<commit-sha>
or .../archive/<commit-sha>.tar.gz and equivalent GitLab/Bitbucket patterns that
include a full commit SHA or explicit commit segment) and reject branch/tag
archive URLs, or remove the fallback and rely on upstream-produced gitHosted:
true where available. Ensure the matcher validates a commit SHA (40 hex chars or
valid shortened SHA) in the URL path so mutable archives are not treated as
safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/require-tarball-integrity.md:
- Line 6: Replace the lowercase platform name in the release note prose: find
the occurrence of "github.com" inside the .changeset markdown content (the
sentence listing exempt git-hosted tarballs) and change it to the official
capitalization "GitHub" so the text reads "GitHub / bitbucket.org / gitlab.com"
(or similar) for consistent branding.

---

Outside diff comments:
In `@lockfile/utils/src/toLockfileResolution.ts`:
- Around line 77-83: The isGitHostedTarballUrl predicate is too permissive (host
+ 'tar.gz') and must be tightened to only accept commit-pinned tarball shapes or
be removed in favor of an explicit gitHosted flag; update the
isGitHostedTarballUrl function to match commit-specific URL patterns (e.g.,
codeload.github.com/.../tar.gz/<commit-sha> or .../archive/<commit-sha>.tar.gz
and equivalent GitLab/Bitbucket patterns that include a full commit SHA or
explicit commit segment) and reject branch/tag archive URLs, or remove the
fallback and rely on upstream-produced gitHosted: true where available. Ensure
the matcher validates a commit SHA (40 hex chars or valid shortened SHA) in the
URL path so mutable archives are not treated as safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e9afff73-2e6f-4552-9dba-8999ad8ce147

📥 Commits

Reviewing files that changed from the base of the PR and between e3321b1 and cb1c924.

📒 Files selected for processing (4)
  • .changeset/require-tarball-integrity.md
  • lockfile/utils/src/pkgSnapshotToResolution.ts
  • lockfile/utils/src/toLockfileResolution.ts
  • lockfile/utils/test/pkgSnapshotToResolution.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)

Files:

  • lockfile/utils/src/toLockfileResolution.ts
  • lockfile/utils/test/pkgSnapshotToResolution.ts
  • lockfile/utils/src/pkgSnapshotToResolution.ts
🧠 Learnings (1)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • lockfile/utils/src/toLockfileResolution.ts
  • lockfile/utils/test/pkgSnapshotToResolution.ts
  • lockfile/utils/src/pkgSnapshotToResolution.ts
🪛 LanguageTool
.changeset/require-tarball-integrity.md

[uncategorized] ~6-~6: The official name of this software platform is spelled with a capital “H”.
Context: ...(gitHosted: true or a URL on codeload.github.com / bitbucket.org / gitlab.com) and `...

(GITHUB)

Comment thread .changeset/require-tarball-integrity.md
zkochan added 4 commits May 26, 2026 21:00
…solutions

A tampered lockfile that strips the `integrity` field from a tarball
resolution let the worker download the URL contents and mint a fresh
integrity from the unverified bytes, so an attacker who could also
serve content at the referenced URL would install a tampered package
without any error — including under `--frozen-lockfile`. pnpm now
rejects such entries at lockfile-read time with
`ERR_PNPM_MISSING_TARBALL_INTEGRITY`, matching pacquet's existing
`pacquet_package_manager::missing_tarball_integrity` guard.
…n in snapshot_cache_key

Match the upstream guard landed alongside #11966
(`lockfile/utils/src/pkgSnapshotToResolution.ts`) with a test on
the pacquet side: a `LockfileResolution::Tarball` with `integrity:
None` — what a tampered lockfile looks like — must short-circuit
the warm-batch cache-key derivation by surfacing
`InstallPackageBySnapshotError::MissingTarballIntegrity`. The
structural guard already existed but had no negative test.
…tegrity guard

The strict guard added in the parent commit broke pnpm's own
`with-git-protocol-dep` and `with-non-package-dep` fixtures: the
install pipeline writes git-hosted tarball entries (codeload.github.com
URLs) to the lockfile without an `integrity:` line, because the commit
SHA in the URL is the integrity anchor — git's content-addressed model
binds the bytes to the commit, so a separate hash adds nothing.

Exempt git-hosted tarballs (detected either via the `gitHosted: true`
flag or a URL on the known git hosts, matching the URL fallback in
`toLockfileResolution`) and `file:` tarballs (local paths the user
already controls). The strict check still fires for any other remote
tarball — which is where the AutoFyn-reported vector actually
manifests.

Also export `isGitHostedTarballUrl` from `toLockfileResolution.ts` so
the URL fallback can be shared rather than duplicated.
… intent

Per the repo convention that tests are documentation, the test name
and body already cover what's being asserted; the prior comment
duplicated that. Keep only the non-obvious why: why this guard exists
at the cache-key site at all (warm-batch short-circuit) when the
install-side check also rejects the same input.
@zkochan

zkochan commented May 26, 2026

Copy link
Copy Markdown
Member Author

Responses to the two review-body comments:

Nitpick on pacquet/crates/package-manager/src/create_virtual_store/tests.rs (test doc comment)

Addressed in 1fb76af — trimmed the doc to a single "why this site has a check at all" sentence (the warm-batch short-circuit), since the test name + body already cover what's being asserted.

Major (changes-requested) on lockfile/utils/src/toLockfileResolution.ts (tighten URL detection to commit-SHA-pinned shapes)

Skipping in this PR with reason: the predicate host + tar.gz is the existing isGitHostedTarballUrl convention pnpm already uses at lockfile-write time (back-filling the gitHosted flag on legacy entries — toLockfileResolution.ts:27-28). Pacquet uses the identical loose pattern at pacquet/crates/lockfile/src/resolution.rs:322-327. Tightening it only on my new read-side exemption would mean:

  • Read-side rejects a URL that the write-side classifies as git-hosted (an asymmetry that's worse than the current state).
  • pnpm and pacquet diverge on what counts as a git-hosted URL (parity break).

The corner case is real (a branch/tag tarball URL like .../tar.gz/main is mutable and shouldn't be exempt from integrity), but in practice pnpm's git resolver always pins to a full commit SHA before writing the lockfile — the affected fixtures (with-git-protocol-dep, with-non-package-dep) carry full 40-char SHAs. Hand-authored lockfiles with branch refs are the only realistic path to a mutable git-hosted URL slipping through, and that's a separate hardening that should land at isGitHostedTarballUrl in both pnpm and pacquet together.


Written by an agent (Claude Code, claude-opus-4-7).

@zkochan zkochan merged commit e55f4b5 into main May 26, 2026
25 checks passed
@zkochan zkochan deleted the fix-vuln5 branch May 26, 2026 21:15
zkochan added a commit that referenced this pull request May 27, 2026
…ort #11966 to v10) (#12007)

A tampered `pnpm-lock.yaml` that strips the `integrity` field from a
tarball resolution let the worker download the URL contents and mint a
fresh integrity from the unverified bytes. An attacker who could both
alter the lockfile (e.g. via a pull request that drops `integrity:`)
and serve modified content at the referenced tarball URL could install
a tampered package without any error — including under
`--frozen-lockfile`.

`pkgSnapshotToResolution` now fails closed at lockfile-read time with
`ERR_PNPM_MISSING_TARBALL_INTEGRITY` whenever a tarball-shaped
resolution (no `type` field — covers plain remote, registry-derived,
`file:`, and `gitHosted` entries) lacks integrity. Git-hosted
tarballs and `file:` tarballs remain exempt: the commit SHA in a
git-host URL and the user-controlled local path already anchor the
bytes.

The fix sits at the lockfile-read chokepoint every install path flows
through (deps-resolver, deps-restorer, graph-builder), so both
isolated and hoisted node-linkers are covered.

Credit to AutoFyn for finding and reporting the issue.
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.

2 participants