Introduction
In this project's pnpm-workspace.yaml, we discovered a high-severity configuration gap: the workspace had no minimumReleaseAge setting, meaning that every time a developer ran pnpm install, the resolver could pull in npm packages published just seconds earlier. In a web application where XSS and injection vulnerabilities can directly affect end users, a single compromised dependency is all it takes for an attacker to inject malicious code into production.
The vulnerable configuration file was deceptively simple—just three lines defining workspace packages:
packages:
- "."
- "packages/*"
No guardrails. No quarantine. No time buffer between a package being published and your project consuming it. This is the exact window that supply chain attackers exploit.
This matters because the broader project includes sensitive components: an admin route handler at website/server/src/routes/admin.ts, OAuth token storage in plugins/auth-oauth2/src/store.ts, and a Rust/Tauri backend. A compromised dependency anywhere in this workspace could access tokens, inject scripts into the admin interface, or exfiltrate credentials—all before anyone notices the malicious package on npm.
The Vulnerability Explained
What Is minimumReleaseAge and Why Does It Matter?
Starting with pnpm v10.16.0, the minimumReleaseAge setting tells pnpm's dependency resolver to ignore package versions that were published less than N minutes ago. Without it, the default is effectively zero—any version published at any time is fair game.
Here's the vulnerable pnpm-workspace.yaml in its entirety:
packages:
- "."
- "packages/*"
That's it. No security settings whatsoever.
The Attack Scenario
Consider this realistic attack chain against this specific project:
-
Reconnaissance: An attacker identifies that this workspace depends on a popular utility package (say, a markdown parser used by the website).
-
Typosquatting or account takeover: The attacker publishes a malicious version of a dependency—either through a typosquatted package name or by compromising a maintainer's npm account. The malicious version includes a postinstall script that:
- Reads OAuth tokens fromplugins/auth-oauth2/src/store.ts's plaintext storage
- Exfiltrates data from the admin routes defined inwebsite/server/src/routes/admin.ts
- Installs a persistent backdoor in the Tauri application -
Immediate resolution: A developer on the team runs
pnpm installorpnpm update. Because there's nominimumReleaseAge, pnpm immediately resolves the malicious version. -
Compromise: The malicious code executes during installation or at runtime. Since this is a web application, the attacker could inject XSS payloads that affect every end user.
The critical window here is the time between publication and detection. Most malicious npm packages are caught and removed within hours to days. The minimumReleaseAge setting turns this detection window into a defense window.
Real-World Precedent
This isn't theoretical. The npm ecosystem has seen numerous supply chain attacks:
event-stream(2018): A maintainer handed off a popular package to an attacker who injected cryptocurrency-stealing code.ua-parser-js(2021): A hijacked package with 8 million weekly downloads distributed cryptominers.colorsandfaker(2022): A maintainer sabotaged their own widely-used packages.
In each case, a time-based quarantine would have given the community time to detect and flag the compromise before most projects consumed it.
The Fix
The fix is a single, surgical addition to pnpm-workspace.yaml:
Before (Vulnerable)
packages:
- "."
- "packages/*"
After (Hardened)
packages:
- "."
- "packages/*"
# Wait seven days before resolving a newly published version.
minimumReleaseAge: 10080
What Changed and Why
The value 10080 represents 10,080 minutes, which equals exactly seven days. This means:
- When
pnpm installresolves dependencies, any package version published less than 7 days ago is invisible to the resolver. - If a malicious package is published on Monday, your project won't even consider it until the following Monday—by which time it will almost certainly have been detected, reported, and removed from the npm registry.
- The comment
# Wait seven days before resolving a newly published version.makes the intent clear to every developer who reads the configuration.
Why Seven Days?
Seven days strikes a balance between security and practicality:
- Too short (e.g., 1 day): Many malicious packages survive 24 hours before detection.
- Too long (e.g., 30 days): Legitimate security patches would be delayed, potentially leaving known vulnerabilities unpatched.
- Seven days: Aligns with the typical detection and response cycle for malicious npm packages, while still allowing timely access to legitimate updates.
Behavior Preservation
This change is purely additive and defensive. It does not:
- Break any existing dependency resolution for already-published packages
- Affect packages already in the lockfile
- Change the workspace package structure
- Require any code changes in the application
It only prevents the resolution of brand-new package versions that haven't had time to be vetted by the community.
Prevention & Best Practices
1. Always Set minimumReleaseAge in pnpm Workspaces
For any pnpm v10.16.0+ project, add this to your pnpm-workspace.yaml:
minimumReleaseAge: 10080
2. Layer Your Supply Chain Defenses
No single measure is sufficient. Combine:
| Defense | What It Protects Against |
|---|---|
minimumReleaseAge |
Newly published malicious packages |
Lockfiles (pnpm-lock.yaml) |
Unexpected version changes |
npm audit / pnpm audit |
Known vulnerabilities in dependencies |
| Semgrep config rules | Missing security settings |
| Dependency review (GitHub) | Malicious code in PRs |
| Package signature verification | Tampered packages |
3. Scan Configuration Files, Not Just Code
Traditional SAST tools focus on source code. But as this vulnerability shows, configuration files are attack surface too. Use tools like Semgrep with rules that cover package manager configurations:
semgrep --config "p/supply-chain" pnpm-workspace.yaml
4. Monitor for New pnpm Security Features
pnpm is actively adding supply chain protections. Stay current with:
- pnpm Settings Documentation
- pnpm Release Notes
5. Apply Defense in Depth for Sensitive Components
This project stores OAuth tokens in plaintext (plugins/auth-oauth2/src/store.ts) and has admin routes (website/server/src/routes/admin.ts). A compromised dependency could target these directly. Beyond minimumReleaseAge, consider:
- Encrypting stored credentials (the project already has PBKDF2 available in Rust dependencies)
- Applying least-privilege principles to admin routes
- Sandboxing dependency installation scripts
Key Takeaways
pnpm-workspace.yamlwithoutminimumReleaseAgeis an exploit primitive: It allows attackers to have their malicious packages immediately consumed by your project, with zero quarantine period.- Configuration files are security-critical attack surface: This vulnerability wasn't in application code—it was in a 3-line YAML file that controls how every dependency in the workspace is resolved.
- Seven days (
10080minutes) is the recommended quarantine threshold: It balances security (time for community detection of malicious packages) with practicality (timely access to legitimate updates). - Supply chain hardening is especially critical for web applications: This project's admin routes and OAuth token handling mean a compromised dependency could directly impact end users through XSS, credential theft, or backdoors.
- Semgrep's
pnpm-minimum-release-agerule catches this automatically: Static analysis isn't just for code—it can enforce security invariants in configuration files too.
How Orbis AppSec Detected This
- Source: The npm package registry, where newly published (and potentially malicious) package versions are available for immediate resolution by pnpm.
- Sink: The
pnpm-workspace.yamlconfiguration at line 1, which controls dependency resolution for the entire workspace including sensitive components likewebsite/server/src/routes/admin.tsandplugins/auth-oauth2/src/store.ts. - Missing control: No
minimumReleaseAgesetting was configured, meaning pnpm's resolver had no time-based quarantine for newly published package versions. - CWE: CWE-829 — Inclusion of Functionality from Untrusted Control Sphere.
- Fix: Added
minimumReleaseAge: 10080topnpm-workspace.yaml, enforcing a mandatory 7-day waiting period before any newly published package version can be resolved during installation.
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
Supply chain security isn't glamorous, but it's foundational. A missing three-line configuration in pnpm-workspace.yaml left this entire workspace—admin routes, OAuth tokens, Tauri backend, and all—exposed to any malicious package published to npm. The fix was trivially simple: minimumReleaseAge: 10080. But the protection it provides is profound: a seven-day buffer that transforms the community's detection capability into your project's defense.
The lesson is clear: audit your configuration files with the same rigor you apply to your code. Tools like Semgrep and Orbis AppSec can catch these gaps automatically, but every developer should understand that pnpm-workspace.yaml, package.json, and similar files are part of your security boundary—not just your build system.