<!-- maintainer-action:start -->
## Maintainer Action Plan
This report is ready to review with the shared patch branch. Start with the PR and the expected fixed behavior, then use the detailed exploit narrative below only if you want to replay the original path.
- Advisory: `CAND-PNPM-122` / `GHSA-3qhv-2rgh-x77r`
- Advisory URL: https://github.com/pnpm/pnpm/security/advisories/GHSA-3qhv-2rgh-x77r
- Shared patch PR: https://github.com/pnpm/pnpm-ghsa-j2hc-m6cf-6jm8/pull/1
- Shared patch branch: `security/ghsa-batch-2026-06-09`
- Patch commit: `a93449314f398cf4bdf2e28d033c02d37395ad22`
- Base commit: `origin/main` `55a4035abf1ae3fe7208ba1f5ef43c5eff58ccec`
- Maintainer priority: `start-here`
- Component: `pnpm config/env replacement and registry auth`
- Patch area: project .npmrc env placeholders are not expanded into registry/auth destinations
- Affected packages: `npm:pnpm`, `npm:@pnpm/config.reader`, `rust:pacquet`
- CWE IDs: `CWE-201`, `CWE-200`, `CWE-522`
- Conservative CVSS: `6.5` / `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N`
- Next action: review the shared patch branch for this component, set the final affected version range, merge and release the fix, then publish or close the advisory.
### Expected Patched Behavior
Project `.npmrc` environment placeholders do not expand into registry or auth destinations; the secret is absent from the request URL and auth header.
### Files And Tests To Review
- `config/reader/src/loadNpmrcFiles.ts`
- `config/reader/src/getOptionsFromRootManifest.ts`
- `config/reader/test/index.ts`
- `config/reader/test/getOptionsFromRootManifest.test.ts`
- `pacquet/crates/config/src/npmrc_auth.rs`
- `pacquet/crates/config/src/npmrc_auth/tests.rs`
- `pacquet/crates/config/src/workspace_yaml.rs`
- `pacquet/crates/config/src/workspace_yaml/tests.rs`
- `.changeset/sharp-registry-env-placeholders.md`
### Focused Validation
Run these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed.
```bash
./node_modules/.bin/tsgo --build config/reader/tsconfig.json
NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/getOptionsFromRootManifest.test.ts --runInBand
NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "project \.npmrc does not expand env variables in registry URLs|project \.npmrc does not expand env variables in scoped registry URLs or URL-scoped keys|project \.npmrc does not expand env variables in auth values|user \.npmrc may expand env variables in registry URLs|drops the placeholder when the env var is unset|substitutes normally when the env var is set|only drops the unresolved placeholder|explicit .*undefined.* fallbacks|pnpm-workspace\.yaml registries do not expand env variables|return a warning when the \.npmrc has an env variable" --runInBand
./node_modules/.bin/eslint config/reader/src/loadNpmrcFiles.ts config/reader/src/getOptionsFromRootManifest.ts config/reader/test/index.ts config/reader/test/getOptionsFromRootManifest.test.ts
cargo fmt --manifest-path pacquet/crates/config/Cargo.toml --check
cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_registry_urls --lib
cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_scoped_registry_urls --lib
cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_url_scoped_keys --lib
cargo test --manifest-path pacquet/crates/config/Cargo.toml project_ini_ignores_env_placeholders_in_auth_values --lib
cargo test --manifest-path pacquet/crates/config/Cargo.toml trusted_ini_expands_env_placeholders_in_registry_urls --lib
cargo test --manifest-path pacquet/crates/config/Cargo.toml ignores_env_vars_inside_workspace_registry_values --lib
git diff --check
cargo fmt --check
```
The full patched replay for the shared branch passed with all 20 candidates marked fixed. This candidate's replay evidence is `results/CAND-PNPM-122-patched-result.json`.
<!-- maintainer-action:end -->
# CAND-PNPM-122: Repository config can expand victim environment secrets into registry requests before scripts run
## Advisory Details
### Summary
pnpm and pacquet expanded `${ENV_VAR}` placeholders from repository-controlled `.npmrc` and `pnpm-workspace.yaml` into registry request destinations and registry credentials. A malicious repository could cause dependency resolution to send victim environment secrets to an attacker-selected registry before lifecycle scripts run.
### Details
The vulnerable TypeScript pnpm path was:
- `config/reader/src/loadNpmrcFiles.ts` loaded project `.npmrc` and substituted environment placeholders in keys and values.
- `config/reader/src/getOptionsFromRootManifest.ts` substituted environment placeholders inside workspace `registry`, `regis
## Summary
pnpm's tarball extraction worker skips integrity verification when the `integrity` field is absent from the lockfile resolution. If an attacker can both modify `pnpm-lock.yaml` to remove the `integrity:` field and cause the referenced registry URL to serve altered package content, `pnpm install --frozen-lockfile` can install the altered package without an integrity error. npm's `npm ci` enforces integrity by default; pnpm's behavior of silently skipping verification is a pnpm-specific fail-open gap.
## Vulnerability Details
The `addTarballToStore` function in `worker/src/start.ts` (lines 189-204) checks `if (integrity)` before verifying the tarball hash. The `TarballResolution` type declares `integrity` as optional (`integrity?: string`). When the lockfile omits the `integrity` field, the guard evaluates to `false`, skipping hash verification entirely. The worker then computes a new hash from the unverified content and stores it as legitimate.
```typescript
// worker/src/start.ts:189-204
function addTarballToStore ({ buffer, storeDir, integrity, ... }: TarballExtractMessage) {
if (integrity) { // false when integrity is undefined
const { algorithm, hexDigest } = parseIntegrity(integrity)
const calculatedHash = crypto.hash(algorithm, buffer, 'hex')
if (calculatedHash !== hexDigest) {
return { status: 'error', error: { type: 'integrity_validation_failed', ... } }
}
}
return {
status: 'success',
value: { integrity: integrity ?? calcIntegrity(buffer) },
}
}
```
## Proof of Concept
```bash
bash autofyn_audit/exploits/vuln1_integrity_bypass/exploit.sh
# Publishes a package, generates lockfile, republishes tampered version,
# strips integrity field, re-runs install --frozen-lockfile.
# Result: PASS -- tampered package installed without integrity error.
```
## Impact
Supply chain compromise in environments where an attacker can both alter the lockfile and cause the referenced registry URL to serve altered package content. The `--frozen-lockfile` flag does not fail closed when the integrity field is missing.
## Suggested Remediation
Require an `integrity` field for remote tarball resolutions. Change the `if (integrity)` guard to fail when integrity is absent for non-local packages. When `--frozen-lockfile` is active, reject lockfile entries that lack integrity for remote packages.
---
> Discovered by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn)
> Full audit report: [audit_report.md](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/audit_report.md)
> Exploit script: [exploit.sh](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/exploits/vuln1_integrity_bypass/exploit.sh)
## Summary
pnpm's patch application pipeline (`@pnpm/patch-package`) performs no path validation on file paths extracted from `.patch` files. An attacker who contributes a malicious patch file via a pull request can write attacker-controlled content to or delete arbitrary files on the filesystem during `pnpm install`, as the user running the install. The `diff --git` header paths containing `../../` sequences traverse out of the package directory, and the traversal is difficult to catch in code review because patch file diff headers are opaque to most reviewers.
## Vulnerability Details
During `pnpm install`, when a `patchedDependencies` entry is present in `pnpm-workspace.yaml`, pnpm reads the referenced `.patch` file and applies it via the embedded `@pnpm/patch-package` library. The `applyPatchToDir` function at `patching/apply-patch/src/index.ts:12-13` calls `process.chdir(opts.patchedDir)`, setting the working directory to the installed package location deep inside `node_modules/.pnpm/`.
The patch parser at `@pnpm/patch-package/dist/patch/parse.js:88` extracts file paths from `diff --git a/(.*?) b/(.*?)` headers using a regex with no path sanitization. The `executeEffects` function in `apply.js` then operates on these unsanitized paths:
**File write** (`apply.js:35-49`):
```javascript
case 'file creation': {
const eff = effect
fs.ensureDirSync(dirname(eff.path))
fs.writeFileSync(eff.path, fileContents, { mode: eff.mode })
break
}
```
**File delete** (`apply.js:13-22`):
```javascript
case 'file deletion': {
const eff = effect
// TODO: integrity checks
if (!opts.dryRun) {
fs.unlinkSync(eff.path)
}
break
}
```
A path like `../../../../../../../../../../home/user/.ssh/authorized_keys` in the patch header traverses out of the package directory to an arbitrary location.
## Proof of Concept
```bash
# Write variant:
bash autofyn_audit/exploits/vuln6_patch_traversal_write/exploit.sh
# Result: PASS -- /tmp/vuln6_pwned created with attacker-controlled content
# Delete variant:
bash autofyn_audit/exploits/vuln7_patch_traversal_delete/exploit.sh
# Result: PASS -- /tmp/vuln7_target deleted by malicious patch
# Combined chain (delete + replace SSH authorized_keys):
bash autofyn_audit/exploits/chain2_patch_ssh_backdoor/exploit.sh
# Result: PASS -- authorized_keys replaced with attacker's public key
```
## Impact
Arbitrary file write and delete as the user running `pnpm install`, limited to paths writable by that user. An attacker who submits a PR adding a `.patch` file and `patchedDependencies` config can target SSH authorized_keys, shell configuration, CI/CD files, or other writable files. Patch files may receive less review scrutiny than `package.json` changes because the `../` traversal sequences are in `diff --git` headers that look like patch metadata.
## Suggested Remediation
Validate parsed patch file paths against the package root directory. Reject any path that resolves outside the patched package directory via `path.resolve` + prefix check. Alternatively, sanitize at parse time by rejecting paths containing `..` components in `parse.js`.
---
> Discovered by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn)
> Full audit report: [audit_report.md](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/audit_report.md)
> Exploit script: [exploit.sh](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/exploits/vuln6_patch_traversal_write/exploit.sh)
## Summary
pnpm passes the lockfile-controlled git `resolution.commit` value to `git fetch` without a `--` separator or commit-format validation. For git dependencies fetched through the shallow-fetch path, a malicious lockfile can replace the expected 40-character commit hash with a Git option such as `--upload-pack=<command>`. For SSH and local transports, `--upload-pack` can execute the supplied command. HTTPS transports ignore `--upload-pack`, so the practical attack surface is primarily SSH or local git dependencies.
## Vulnerability Details
The vulnerable path is in `fetching/git-fetcher/src/index.ts`. When a git dependency host is configured for shallow fetching, pnpm calls:
```typescript
await execGit(['fetch', '--depth', '1', 'origin', resolution.commit], { cwd: tempLocation })
```
Because `resolution.commit` is appended before a `--` separator, Git can parse a commit value beginning with `-` as an option. The same file later passes the value to `git checkout` without a separator:
```typescript
await execGit(['checkout', resolution.commit], { cwd: tempLocation })
```
`resolution.commit` comes from the lockfile and is typed as a plain `string`; pnpm does not validate it as a 40-character hexadecimal commit before passing it to Git.
## Proof of Concept
```bash
bash autofyn_audit/exploits/vuln11_git_upload_pack_rce/exploit.sh
# Creates a local bare git repo and triggers the shallow-fetch path.
# Replaces the lockfile commit hash with '--upload-pack=touch /tmp/vuln11_pwned'.
# Result: PASS -- /tmp/vuln11_pwned created by injected touch command.
```
The PoC uses a local `file://githost/...` repository because the injection requires a local or SSH transport. HTTPS transport ignores `--upload-pack`.
## Impact
Code execution as the user running `pnpm install`, under specific transport conditions. The attacker must modify `pnpm-lock.yaml`, and the affected dependency must use SSH or local git transport. HTTPS transport (the common case) is immune.
## Suggested Remediation
Add a `--` separator before lockfile-controlled git revision values. Validate `resolution.commit` matches `/^[0-9a-f]{40}$/i` before passing to Git.
---
> Discovered by [AutoFyn](https://github.com/SignalPilot-Labs/AutoFyn)
> Full audit report: [audit_report.md](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/audit_report.md)
> Exploit script: [exploit.sh](https://github.com/tempcollab/pnpm/blob/main/autofyn_audit/exploits/vuln11_git_upload_pack_rce/exploit.sh)
### Summary
A malicious `codeload.github.com` server can serve whatever tarball it wants and pnpm will install it regardless of the lockfile.
### Details
The lockfile does not store the hash of the dependencies from https://codeload.github.com
This means that if this server was compromised or a person's machine configuration was compromised, pnpm would download and install these dependencies.
### PoC
```sh
> pnpm -v
10.28.2
```
Given the following package.json:
```json
{
"dependencies": {
"add": "git://github.com/dsherret/npm-git-dep.git#b3eeb9b"
}
}
```
This produces a lockfile like so:
```yaml
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
add:
specifier: git://github.com/dsherret/npm-git-dep.git#b3eeb9b
version: https://codeload.github.com/dsherret/npm-git-dep/tar.gz/b3eeb9b
packages:
add@https://codeload.github.com/dsherret/npm-git-dep/tar.gz/b3eeb9b:
resolution: {tarball: https://codeload.github.com/dsherret/npm-git-dep/tar.gz/b3eeb9b}
version: 1.0.0
snapshots:
add@https://codeload.github.com/dsherret/npm-git-dep/tar.gz/b3eeb9b: {}
```
Notice that there is no hash. The `b3eeb9b` is not sufficient because I can configure my machine to resolve a compromised tarball from that url (I tested it out and pnpm just installs it).
### Impact
Anyone relying on github git dependencies.
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires loading images where the declared size values are much too large compared to the actual data.
### Patches
This has been fixed in [pypdf==6.14.0](https://github.com/py-pdf/pypdf/releases/tag/6.14.0).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3888](https://github.com/py-pdf/pypdf/pull/3888).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to long runtimes. This requires cross-reference streams with repeated malformed cross-reference streams.
### Patches
This has been fixed in [pypdf==6.14.0](https://github.com/py-pdf/pypdf/releases/tag/6.14.0).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3887](https://github.com/py-pdf/pypdf/pull/3887).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to an infinite loop. This requires parsing the content stream of a page with a not terminated inline image, as done when extracting the page text for example.
### Patches
This has been fixed in [pypdf==6.14.1](https://github.com/py-pdf/pypdf/releases/tag/6.14.1).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3891](https://github.com/py-pdf/pypdf/pull/3891).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to an infinite loop. This requires parsing the content stream of a page with a not terminated inline image, as done when extracting the page text for example. It only affects the ASCII85 and ASCIIHex filters.
### Patches
This has been fixed in [pypdf==6.14.2](https://github.com/py-pdf/pypdf/releases/tag/6.14.2).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3892](https://github.com/py-pdf/pypdf/pull/3892).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to large memory usage, as `MAX_DECLARED_STREAM_LENGTH` is sometimes ignored. This requires parsing a content stream without a `/Length` value.
### Patches
This has been fixed in [pypdf==6.13.3](https://github.com/py-pdf/pypdf/releases/tag/6.13.3).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3871](https://github.com/py-pdf/pypdf/pull/3871).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to long runtimes. This requires accessing a stream which uses the `/FlateDecode` filter with a PNG predictor.
### Patches
This has been fixed in [pypdf==6.12.2](https://github.com/py-pdf/pypdf/releases/tag/6.12.2).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3806](https://github.com/py-pdf/pypdf/pull/3806).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing large XMP metadata, possibly with lots of unnecessary elements.
### Patches
This has been fixed in [pypdf==6.12.1](https://github.com/py-pdf/pypdf/releases/tag/6.12.1).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3796](https://github.com/py-pdf/pypdf/pull/3796).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to the RAM being exhausted. This requires accessing an image using `/FlateDecode` with large size values.
### Patches
This has been fixed in [pypdf==6.10.2](https://github.com/py-pdf/pypdf/releases/tag/6.10.2).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3734](https://github.com/py-pdf/pypdf/pull/3734).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to the RAM being exhausted. This requires accessing a stream compressed using `/FlateDecode` with a `/Predictor` unequal 1 and large predictor parameters.
### Patches
This has been fixed in [pypdf==6.10.2](https://github.com/py-pdf/pypdf/releases/tag/6.10.2).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3734](https://github.com/py-pdf/pypdf/pull/3734).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to long runtimes. This requires cross-reference streams with wrong large `/Size` values or object streams with wrong large `/N` values.
### Patches
This has been fixed in [pypdf==6.10.1](https://github.com/py-pdf/pypdf/releases/tag/6.10.1).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3733](https://github.com/py-pdf/pypdf/pull/3733).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing the XMP metadata.
### Patches
This has been fixed in [pypdf==6.10.0](https://github.com/py-pdf/pypdf/releases/tag/6.10.0).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3724](https://github.com/py-pdf/pypdf/pull/3724).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to long runtimes and/or large memory usage. This requires accessing an array-based stream with lots of entries.
### Patches
This has been fixed in [pypdf==6.9.1](https://github.com/py-pdf/pypdf/releases/tag/6.9.1).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3686](https://github.com/py-pdf/pypdf/pull/3686).
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to large memory usage. This requires parsing a content stream with a rather large `/Length` value, regardless of the actual data length inside the stream.
### Patches
This has been fixed in [pypdf==6.8.0](https://github.com/py-pdf/pypdf/releases/tag/6.8.0).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3675](https://github.com/py-pdf/pypdf/pull/3675).
As far as we are aware, this mostly affects reading from buffers of unknown size, as returned by `open("file.pdf", mode="rb")` for example. Passing a file path or a `BytesIO` buffer to *pypdf* instead does not seem to trigger the vulnerability.
### Impact
An attacker who uses this vulnerability can craft a PDF which leads to long runtimes. This requires accessing a stream which uses the `/ASCIIHexDecode` filter.
### Patches
This has been fixed in [pypdf==6.7.5](https://github.com/py-pdf/pypdf/releases/tag/6.7.5).
### Workarounds
If you cannot upgrade yet, consider applying the changes from PR [#3666](https://github.com/py-pdf/pypdf/pull/3666).