The Silent Risk in Your pnpm Workspace
Every time a developer runs pnpm install, pnpm reaches out to the npm registry and resolves the best matching version of each dependency. By default, it will happily install a package version that was published seconds ago — no questions asked. That zero-delay trust is exactly what supply chain attackers count on.
In a recent security audit of a Node.js library's pnpm-workspace.yaml, Orbis AppSec flagged a high-severity misconfiguration: the workspace had no minimumReleaseAge setting. A single line was missing, and its absence meant that any newly published npm package — including a typosquatted, dependency-confused, or account-hijacked one — could be pulled into the build immediately after publication, with no waiting period for the community or security tooling to catch it.
The Vulnerability Explained
What minimumReleaseAge Does (and Doesn't Do by Default)
pnpm v10.16.0 introduced the minimumReleaseAge setting in pnpm-workspace.yaml. When set, it instructs pnpm to refuse to install any package version that was published to the npm registry more recently than the specified number of minutes ago. It acts as a quarantine window.
When this setting is absent — as it was in this project — pnpm's default behavior is to install any version as soon as it is available on the registry. Here is what the vulnerable configuration looked like:
# pnpm-workspace.yaml (before fix)
allowBuilds:
puppeteer: false
unrs-resolver: false
minimumReleaseAgeExclude:
- "@qlik/*"
Notice that minimumReleaseAgeExclude is present — a list of packages exempted from the release age check — but there is no minimumReleaseAge value to apply in the first place. This is a subtle but critical gap: the exclusion list is defined, but the rule it references does not exist.
How This Gets Exploited
Supply chain attacks targeting npm have grown dramatically in sophistication. The attack patterns that minimumReleaseAge defends against include:
Dependency Confusion: An attacker publishes a public npm package with the same name as a private internal package, hoping the registry resolves to theirs first. The window of maximum risk is the first few hours after publication, before security researchers or automated scanners flag it.
Account Takeover / Maintainer Compromise: A legitimate package maintainer's npm account is compromised, and the attacker publishes a malicious patch version (e.g., lodash@4.17.22) containing a backdoor. Without a release age window, this version can be installed the moment it lands.
Typosquatting: A package named expres (missing the s) is published to catch developers who mistype express. New publications are most dangerous before they appear in threat intelligence feeds.
In this specific project — a Node.js library consumed by downstream users — a supply chain compromise would not just affect this repository. It would propagate to every project that depends on this library, multiplying the blast radius significantly.
The Specific Misconfiguration at Line 1
Semgrep's rule package_managers.pnpm.pnpm-missing-minimum-release-age.pnpm-minimum-release-age matched this file at line 1 because the entire pnpm-workspace.yaml lacks the minimumReleaseAge key. The presence of minimumReleaseAgeExclude without a corresponding minimumReleaseAge is particularly misleading — it creates the appearance of a policy while providing none of the protection.
The Fix
The fix is minimal and precise. A single setting was added to pnpm-workspace.yaml:
# pnpm-workspace.yaml (after fix)
allowBuilds:
puppeteer: false
unrs-resolver: false
minimumReleaseAge: 10080
minimumReleaseAgeExclude:
- "@qlik/*"
Before vs. After
| Before | After | |
|---|---|---|
| Setting present | ✗ Missing | ✓ minimumReleaseAge: 10080 |
| Quarantine window | 0 minutes (instant) | 10,080 minutes (7 days) |
| Exclusions active | Defined but inert | Correctly applied to @qlik/* |
| Supply chain risk | High | Significantly reduced |
Why 10080?
10080 is the number of minutes in exactly 7 days (7 × 24 × 60 = 10,080). This is the value recommended in the pnpm documentation and represents a pragmatic balance:
- Long enough for security researchers, automated scanners, and the community to identify malicious packages before they reach your build.
- Short enough that it does not meaningfully delay legitimate dependency updates in most development workflows.
- Consistent with the npm ecosystem's informal "watch period" that many security teams apply manually.
The @qlik/* Exclusion
The existing minimumReleaseAgeExclude entry for @qlik/* packages is now functional. This is appropriate for first-party or highly trusted scoped packages where the team wants to consume updates immediately. The key point is that this exclusion is now a deliberate, documented exception to an active policy — not an orphaned configuration key.
Prevention & Best Practices
1. Enable minimumReleaseAge in Every pnpm Workspace
Any project using pnpm v10.16.0 or later should include this in pnpm-workspace.yaml:
minimumReleaseAge: 10080
If you have packages that legitimately need faster update cycles (internal packages, trusted scoped registries), use minimumReleaseAgeExclude judiciously:
minimumReleaseAge: 10080
minimumReleaseAgeExclude:
- "your-internal-scope/*"
2. Combine with Other Supply Chain Controls
minimumReleaseAge is one layer in a defense-in-depth strategy. Pair it with:
allowBuildsallowlist (already present in this repo): Restricts which packages can run postinstall scripts, preventing malicious build-time code execution.- Lockfile integrity checks: Commit and verify
pnpm-lock.yamlin CI to detect unexpected dependency changes. - Dependency review in PRs: Use tools like
pnpm auditor GitHub's dependency review action to flag new vulnerabilities before merging. - Private registry mirroring: Route package installs through a controlled registry (e.g., Verdaccio, Artifactory) that applies its own vetting policies.
3. Audit Your pnpm-workspace.yaml with Semgrep
The Semgrep rule that caught this issue can be run locally:
semgrep --config "p/supply-chain" pnpm-workspace.yaml
Or add it to your CI pipeline to catch regressions:
# .github/workflows/security.yml
- name: Semgrep scan
uses: semgrep/semgrep-action@v1
with:
config: p/supply-chain
4. Understand the Threat Model for Libraries
This repository is a Node.js library — its consumers inherit its dependency tree. A compromised transitive dependency here does not just affect this project; it affects every downstream application. Library maintainers have a heightened responsibility to apply supply chain controls because their security posture directly impacts their users' security posture.
Relevant Standards
- CWE-1357: Reliance on Insufficiently Trustworthy Component
- OWASP A06:2021: Vulnerable and Outdated Components
- SLSA (Supply Chain Levels for Software Artifacts): Recommends provenance verification and controlled dependency ingestion
Key Takeaways
minimumReleaseAgeExcludewithoutminimumReleaseAgeis a no-op — the exclusion list inpnpm-workspace.yamlwas defined but had nothing to exclude from, creating a false sense of security.- Zero-delay package installation is the default — pnpm (and npm/yarn) will install packages the moment they appear on the registry unless you explicitly configure a waiting period.
- 10,080 minutes (7 days) is the recommended quarantine window — this matches community expectations for vetting new package versions and is the value specified in pnpm's official documentation.
- Library projects carry amplified supply chain risk — a compromised dependency in a published library propagates to all downstream consumers, making these controls especially important for reusable packages.
- Static analysis can catch configuration-level supply chain gaps — this vulnerability was not in application code but in a YAML configuration file, demonstrating that security scanning must cover infrastructure and tooling configs, not just source code.
How Orbis AppSec Detected This
- Source: The
pnpm-workspace.yamlfile at line 1, which controls how pnpm resolves and installs all workspace dependencies from the npm registry. - Sink: Any
pnpm installinvocation that resolves a newly published package version — the dangerous "call site" is the package resolution step itself, where a freshly published malicious package could be fetched without delay. - Missing control: The
minimumReleaseAgekey was entirely absent frompnpm-workspace.yaml, meaning pnpm applied no time-based trust policy to incoming package versions. - CWE: CWE-1357 — Reliance on Insufficiently Trustworthy Component
- Fix: Added
minimumReleaseAge: 10080topnpm-workspace.yaml, enforcing a 7-day quarantine window before any newly published package version can be installed.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
A single missing line in pnpm-workspace.yaml — minimumReleaseAge: 10080 — was the difference between a workspace that blindly trusts the npm registry in real time and one that applies a 7-day vetting window to every new package version. This is not a theoretical risk: supply chain attacks via freshly published packages are an active and growing threat vector, and the npm ecosystem has seen high-profile incidents of exactly this kind.
The fix is trivially small. The protection it provides is substantial. If your project uses pnpm v10.16.0 or later and your pnpm-workspace.yaml does not include minimumReleaseAge, add it today — especially if you maintain a library consumed by others.