fix: reject path-traversal config dependency names from the env lockfile#12470
Conversation
PR Summary by Qodofix: reject path-traversal config dependency names from the env lockfile Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12470 +/- ##
=======================================
Coverage 88.12% 88.13%
=======================================
Files 311 312 +1
Lines 41924 41970 +46
=======================================
+ Hits 36947 36991 +44
- Misses 4977 4979 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Code review by qodo was updated up to the latest commit 780598a |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughSecurity fix that validates ChangesTypeScript (pnpm) env-installer — validation gates and null-prototype hardening
Rust (pacquet) env-installer — validation at lockfile and manifest entry points
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~28 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
|
Code review by qodo was updated up to the latest commit 3098e41 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@installing/env-installer/src/installConfigDeps.ts`:
- Around line 38-39: Move the assertValidConfigDepNames validation to run before
normalizeForInstall to prevent malicious configDependencies from triggering file
writes. Call assertValidConfigDepNames on the configDepsOrLockfile input before
passing it to normalizeForInstall, ensuring invalid names are rejected before
any side effects like pnpm-lock.yaml writes can occur.
In `@installing/env-installer/test/installConfigDeps.ts`:
- Around line 191-213: The test function `an optional subdep named __proto__ in
the env lockfile is rejected` verifies that installConfigDeps rejects the
invalid name but does not assert that no `__proto__` artifacts were actually
created in the project or store directories. Add assertions after the expect
rejects call to verify that no `__proto__` entry exists under the project root
directory (using rootDir) and store directory (using storeDir), similar to what
other security regression tests do, to ensure the rejection prevented any side
effects.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b93cf90d-c605-41ae-9850-f79b66e6a8b6
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!Cargo.lock
📒 Files selected for processing (8)
.changeset/config-deps-path-traversal.mdinstalling/env-installer/src/installConfigDeps.tsinstalling/env-installer/src/migrateConfigDeps.tsinstalling/env-installer/test/installConfigDeps.tspacquet/crates/env-installer/Cargo.tomlpacquet/crates/env-installer/src/errors.rspacquet/crates/env-installer/src/install_config_deps.rspacquet/crates/env-installer/src/tests.rs
Micro-Benchmark ResultsLinux |
|
Code review by qodo was updated up to the latest commit 041bbfa |
|
Code review by qodo was updated up to the latest commit 17bbb97 |
Integrated-Benchmark Report (Linux)Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.228610667360001,
"stddev": 0.2863907075277005,
"median": 4.138103853760001,
"user": 3.8349984199999994,
"system": 3.4848826600000002,
"min": 3.92397150226,
"max": 4.8223811882600005,
"times": [
4.8223811882600005,
4.56691352226,
4.00394932226,
4.269960715260001,
4.14792561526,
4.02638813526,
4.12828209226,
4.01533750026,
3.92397150226,
4.38099708026
]
},
{
"command": "pacquet@main",
"mean": 4.143054784259999,
"stddev": 0.181230035079206,
"median": 4.08948322376,
"user": 3.7846206199999997,
"system": 3.47733826,
"min": 3.94983425626,
"max": 4.52038788026,
"times": [
4.09104504226,
4.31905707826,
3.96888657026,
4.086230647260001,
3.98869067026,
4.52038788026,
4.08792140526,
4.28989544426,
4.12859884826,
3.94983425626
]
},
{
"command": "pnpr@HEAD",
"mean": 2.28326037276,
"stddev": 0.13855306353258703,
"median": 2.29467456276,
"user": 2.64072392,
"system": 2.97867006,
"min": 2.07149634526,
"max": 2.46813260326,
"times": [
2.14806162026,
2.38910643926,
2.42957223226,
2.19288819826,
2.39107903426,
2.1529181292599997,
2.34861853126,
2.24073059426,
2.46813260326,
2.07149634526
]
},
{
"command": "pnpr@main",
"mean": 2.18732853586,
"stddev": 0.13177144818302666,
"median": 2.20284712476,
"user": 2.61804482,
"system": 2.99213846,
"min": 2.0062820492599998,
"max": 2.36182075926,
"times": [
2.34865212826,
2.11682741326,
2.29857104026,
2.1953509482599998,
2.36182075926,
2.2559607962599997,
2.05983042726,
2.21034330126,
2.0062820492599998,
2.01964649526
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6612121240600001,
"stddev": 0.01725950877371257,
"median": 0.6629280507600002,
"user": 0.36486922,
"system": 1.3750407799999997,
"min": 0.6398598962600001,
"max": 0.6870250762600001,
"times": [
0.6870250762600001,
0.63995411026,
0.6711116882600001,
0.6674251592600001,
0.6584309422600001,
0.6524004482600001,
0.6801791982600001,
0.6737226112600001,
0.6398598962600001,
0.64201211026
]
},
{
"command": "pacquet@main",
"mean": 0.69139275366,
"stddev": 0.1056871528248886,
"median": 0.6666094547600001,
"user": 0.39089202,
"system": 1.34902008,
"min": 0.6287913982600001,
"max": 0.9862641492600002,
"times": [
0.6823940262600001,
0.6287913982600001,
0.68413404726,
0.6513099792600001,
0.6675084172600001,
0.6332086502600001,
0.6657104922600001,
0.67943887426,
0.6351675022600001,
0.9862641492600002
]
},
{
"command": "pnpr@HEAD",
"mean": 0.7140832400600001,
"stddev": 0.05349075565307612,
"median": 0.69799576626,
"user": 0.39021081999999996,
"system": 1.38370878,
"min": 0.6624304392600001,
"max": 0.8535951352600001,
"times": [
0.6856582702600001,
0.7378120632600002,
0.69729495626,
0.7258022852600001,
0.6986965762600001,
0.70470203626,
0.6624304392600001,
0.6938612112600001,
0.8535951352600001,
0.68097942726
]
},
{
"command": "pnpr@main",
"mean": 0.7233416236600002,
"stddev": 0.059912203310515166,
"median": 0.7103703112600002,
"user": 0.39558232000000004,
"system": 1.37935568,
"min": 0.6812326422600001,
"max": 0.8864348052600001,
"times": [
0.7198357812600001,
0.6812326422600001,
0.7036810322600001,
0.7282778762600001,
0.6892391822600001,
0.7170595902600001,
0.6923952792600001,
0.8864348052600001,
0.6869457912600001,
0.7283142562600001
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.370832694000001,
"stddev": 0.04104188196477578,
"median": 4.376998839500001,
"user": 3.7765408199999997,
"system": 3.4388117,
"min": 4.3064371085,
"max": 4.4397487975,
"times": [
4.3720389705,
4.3064371085,
4.4161802015000005,
4.383919517500001,
4.3932918785,
4.3819587085,
4.3466012555,
4.4397487975,
4.3417238595,
4.3264266425
]
},
{
"command": "pacquet@main",
"mean": 4.3661489872,
"stddev": 0.06663233553064815,
"median": 4.370324522000001,
"user": 3.77683442,
"system": 3.4449490999999997,
"min": 4.2339497295,
"max": 4.4845420315,
"times": [
4.4845420315,
4.3427265065,
4.3389130475,
4.3593278045,
4.2339497295,
4.3813212395,
4.3903386685,
4.3176657305,
4.4254536155,
4.3872514985
]
},
{
"command": "pnpr@HEAD",
"mean": 2.2950596298,
"stddev": 0.1291156765472127,
"median": 2.2594385595,
"user": 2.49251562,
"system": 2.9430559,
"min": 2.1098411305,
"max": 2.5286656554999998,
"times": [
2.4147670805,
2.3704174795,
2.2206992194999997,
2.5286656554999998,
2.2260347365,
2.2928423825,
2.1098411305,
2.3940312135,
2.1675991294999997,
2.2256982704999997
]
},
{
"command": "pnpr@main",
"mean": 2.1952805767999997,
"stddev": 0.14434595666662306,
"median": 2.1539372654999998,
"user": 2.4828083199999997,
"system": 2.9477801999999995,
"min": 2.0280975925,
"max": 2.4895537365,
"times": [
2.1510380115,
2.1309630815,
2.1759764644999997,
2.4186617125,
2.4895537365,
2.1568365194999997,
2.0900666264999996,
2.1340732565,
2.0280975925,
2.1775387664999997
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.3596377136400002,
"stddev": 0.015458425959476274,
"median": 1.3535928077400001,
"user": 1.34660292,
"system": 1.73065478,
"min": 1.34018699424,
"max": 1.3861097122400001,
"times": [
1.34986965624,
1.35447972024,
1.3861097122400001,
1.36852666824,
1.35270589524,
1.34018699424,
1.35607298824,
1.38558768324,
1.35061889024,
1.3522189282400001
]
},
{
"command": "pacquet@main",
"mean": 1.3913680320399997,
"stddev": 0.02555071729683921,
"median": 1.38581741524,
"user": 1.3844105199999999,
"system": 1.7468261799999998,
"min": 1.3549965722400001,
"max": 1.43840573024,
"times": [
1.37537698824,
1.43840573024,
1.38215288324,
1.42886113224,
1.3970733152400001,
1.38948194724,
1.3970067292400001,
1.37460620624,
1.3549965722400001,
1.37571881624
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6814755120400001,
"stddev": 0.06657954037249862,
"median": 0.6611613457400001,
"user": 0.33504771999999994,
"system": 1.31193668,
"min": 0.6486177482400001,
"max": 0.87005536624,
"times": [
0.65403764024,
0.66071894024,
0.6580829782400001,
0.87005536624,
0.66160375124,
0.6486177482400001,
0.67162921624,
0.65794911424,
0.66571730224,
0.66634306324
]
},
{
"command": "pnpr@main",
"mean": 0.6641023532400001,
"stddev": 0.016087664186452928,
"median": 0.6588523417400001,
"user": 0.35150832000000004,
"system": 1.30250598,
"min": 0.64860921124,
"max": 0.70665944424,
"times": [
0.66992972324,
0.6660362952400001,
0.70665944424,
0.65817556824,
0.6626604392400001,
0.65899734524,
0.65415110424,
0.6570970632400001,
0.65870733824,
0.64860921124
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.99494486966,
"stddev": 0.02799022708875373,
"median": 2.9891137848600002,
"user": 1.7593895400000001,
"system": 1.9617115600000001,
"min": 2.95430110386,
"max": 3.0597093058600002,
"times": [
2.98224618386,
2.99953274686,
3.00161577886,
2.98065081786,
2.95430110386,
3.01487793086,
2.9930377628600002,
3.0597093058600002,
2.9851898068600002,
2.97828725886
]
},
{
"command": "pacquet@main",
"mean": 3.0201101489599997,
"stddev": 0.027701823062846683,
"median": 3.02676119636,
"user": 1.7826252400000002,
"system": 1.98816526,
"min": 2.97679283786,
"max": 3.05356759386,
"times": [
3.03268801786,
3.00213411286,
3.04556837086,
2.9787482238600003,
3.05356759386,
3.02821424686,
3.02530814586,
3.04926717186,
3.00881276786,
2.97679283786
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6621774786600001,
"stddev": 0.015490088062718375,
"median": 0.6584685783600001,
"user": 0.33137663999999994,
"system": 1.3100333599999998,
"min": 0.6467081918600001,
"max": 0.7032496138600001,
"times": [
0.7032496138600001,
0.6571726258600001,
0.6639399118600001,
0.6513951208600001,
0.66589723586,
0.6608763888600001,
0.6595929968600001,
0.6467081918600001,
0.6555985408600001,
0.65734415986
]
},
{
"command": "pnpr@main",
"mean": 0.6941361933600001,
"stddev": 0.08497046842429093,
"median": 0.6687605893600002,
"user": 0.35596514,
"system": 1.30126686,
"min": 0.6418625258600001,
"max": 0.9324279818600001,
"times": [
0.66616564186,
0.6700336188600001,
0.6762026468600001,
0.6602233958600001,
0.67049574586,
0.6674875598600001,
0.6575626978600001,
0.6418625258600001,
0.6989001188600001,
0.9324279818600001
]
}
]
} |
|
| Branch | pr/12470 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,370.83 ms(+3.61%)Baseline: 4,218.39 ms | 5,062.06 ms (86.34%) |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot 🚷 view threshold | 2,994.94 ms(-0.60%)Baseline: 3,013.03 ms | 3,615.63 ms (82.83%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,359.64 ms(+2.14%)Baseline: 1,331.12 ms | 1,597.35 ms (85.12%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 4,228.61 ms(+0.86%)Baseline: 4,192.59 ms | 5,031.11 ms (84.05%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 661.21 ms(+7.18%)Baseline: 616.90 ms | 740.29 ms (89.32%) |
|
| Branch | pr/12470 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,295.06 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 662.18 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 681.48 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,283.26 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 714.08 ms |
|
Code review by qodo was updated up to the latest commit 2668731 |
2668731 to
06054ce
Compare
|
Code review by qodo was updated up to the latest commit 06054ce |
|
Code review by qodo was updated up to the latest commit 5b9b9f8 |
|
@coderabbitai configuration |
|
Configuration used: Path: .coderabbit.yaml CodeRabbit configurationlanguage: en-US
tone_instructions: ''
early_access: false
enable_free_tier: true
inheritance: false
focus: casual
reviews:
profile: chill
request_changes_workflow: false
high_level_summary: true
high_level_summary_instructions: ''
high_level_summary_placeholder: '`@coderabbitai` summary'
high_level_summary_in_walkthrough: false
auto_title_placeholder: '`@coderabbitai`'
auto_title_instructions: ''
review_status: true
review_details: false
commit_status: true
fail_commit_status: false
collapse_walkthrough: true
changed_files_summary: true
sequence_diagrams: true
estimate_code_review_effort: true
assess_linked_issues: true
related_issues: true
related_prs: true
suggested_labels: true
labeling_instructions:
- label: 'product: pnpm'
instructions: >-
Apply when changes affect the TypeScript pnpm CLI (any code outside
pacquet/ and pnpr/).
- label: 'product: pacquet'
instructions: >-
Apply when changes affect pacquet, the Rust port of the pnpm CLI (the
pacquet/ directory).
- label: 'product: pnpr'
instructions: >-
Apply when changes affect pnpr, the pnpm registry server (the pnpr/
directory and pacquet/crates/pnpr-* crates).
auto_apply_labels: true
suggested_reviewers: true
auto_assign_reviewers: false
suggested_reviewers_instructions: []
in_progress_fortune: true
poem: false
enable_prompt_for_ai_agents: true
path_filters:
- '!pnpm-lock.yaml'
- '!Cargo.lock'
- '!**/dist/**'
- '!**/__fixtures__/**'
- '!**/*.snap'
- '!**/CHANGELOG.md'
path_instructions:
- path: '**/*'
instructions: >
Apply the "AI Review Guidance" section in AGENTS.md. Security
vulnerabilities are the first review priority; performance regressions
in pnpm, pacquet, and pnpr are the second priority. Surface only issues
tied to changed code, and explain the exploit path, impact, or hot path
affected.
abort_on_close: true
disable_cache: false
slop_detection:
enabled: true
auto_review:
enabled: true
description_keyword: ''
auto_incremental_review: true
auto_pause_after_reviewed_commits: 5
ignore_title_keywords: []
labels: []
drafts: false
base_branches: []
ignore_usernames: []
finishing_touches:
docstrings:
enabled: true
unit_tests:
enabled: true
simplify:
enabled: false
custom: []
pre_merge_checks:
override_requested_reviewers_only: false
docstrings:
mode: warning
threshold: 80
title:
mode: warning
requirements: ''
description:
mode: warning
issue_assessment:
mode: warning
custom_checks: []
tools:
ast-grep:
rule_dirs: []
util_dirs: []
essential_rules: true
packages: []
shellcheck:
enabled: true
ruff:
enabled: true
markdownlint:
enabled: true
github-checks:
enabled: true
timeout_ms: 90000
languagetool:
enabled: true
enabled_rules: []
disabled_rules: []
enabled_categories: []
disabled_categories: []
enabled_only: false
level: default
biome:
enabled: true
hadolint:
enabled: true
swiftlint:
enabled: true
phpstan:
enabled: true
level: default
phpmd:
enabled: true
phpcs:
enabled: true
golangci-lint:
enabled: true
yamllint:
enabled: true
gitleaks:
enabled: true
trufflehog:
enabled: true
checkov:
enabled: true
tflint:
enabled: true
detekt:
enabled: true
eslint:
enabled: true
flake8:
enabled: true
fbinfer:
enabled: false
enable_java: false
fortitudeLint:
enabled: true
rubocop:
enabled: true
buf:
enabled: true
regal:
enabled: true
actionlint:
enabled: true
zizmor:
enabled: true
pmd:
enabled: true
clang:
enabled: true
cppcheck:
enabled: true
opengrep:
enabled: true
semgrep:
enabled: true
circleci:
enabled: true
clippy:
enabled: true
sqlfluff:
enabled: true
trivy:
enabled: true
prismaLint:
enabled: true
pylint:
enabled: true
oxc:
enabled: true
shopifyThemeCheck:
enabled: true
luacheck:
enabled: true
brakeman:
enabled: true
dotenvLint:
enabled: true
htmlhint:
enabled: true
stylelint:
enabled: true
checkmake:
enabled: true
osvScanner:
enabled: true
oasdiff:
enabled: true
presidio:
enabled: false
blinter:
enabled: true
smartyLint:
enabled: true
emberTemplateLint:
enabled: true
skillspector:
enabled: true
psscriptanalyzer:
enabled: true
chat:
art: true
allow_non_org_members: true
auto_reply: true
integrations:
jira:
usage: auto
linear:
usage: auto
knowledge_base:
opt_out: false
web_search:
enabled: true
code_guidelines:
enabled: true
filePatterns: []
learnings:
scope: local
approval_delay: 0
issues:
scope: auto
jira:
usage: auto
project_keys: []
linear:
usage: auto
team_keys: []
pull_requests:
scope: auto
mcp:
usage: auto
disabled_servers: []
automatic_repository_linking: false
linked_repositories: []
code_generation:
docstrings:
path_instructions: []
unit_tests:
path_instructions: []
issue_enrichment:
auto_enrich:
enabled: false
planning:
enabled: true
auto_planning:
enabled: true
labels: []
labeling:
labeling_instructions:
- label: 'type: bug'
instructions: Apply when the issue reports incorrect or unexpected behavior.
- label: 'type: feature'
instructions: Apply when the issue requests new functionality or an enhancement.
- label: 'type: question'
instructions: >-
Apply when the issue is a usage question rather than a bug or feature
request.
- label: 'area: lockfile'
instructions: >-
Apply when the issue concerns pnpm-lock.yaml parsing, format, or
handling.
- label: 'area: monorepo'
instructions: Apply when the issue concerns the pnpm workspace feature.
- label: 'area: lifecycle-scripts'
instructions: Apply when the issue concerns running package lifecycle/build scripts.
- label: 'area: peers'
instructions: Apply when the issue concerns peer dependency resolution.
- label: 'area: catalogs'
instructions: Apply when the issue concerns the catalogs feature.
- label: 'area: patching'
instructions: >-
Apply when the issue concerns patching packages (pnpm patch /
patchedDependencies).
- label: 'area: resolution'
instructions: Apply when the issue concerns dependency resolution.
- label: 'area: supply chain security'
instructions: >-
Apply when the issue concerns minimumReleaseAge, blockExoticSubdeps,
build-script safety, or trust policies.
- label: 'area: config dependencies'
instructions: Apply when the issue concerns configDependencies.
auto_apply_labels: true
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
Code review by qodo was updated up to the latest commit c94ecfc |
|
/improve |
|
Code review by qodo was updated up to the latest commit c94ecfc |
|
Code review by qodo was updated up to the latest commit 6664b98 |
|
Code review by qodo was updated up to the latest commit ce6d981 |
Config dependency names (and their optional subdependency names) read from the committed env lockfile (pnpm-lock.yaml) were used directly to build filesystem paths under node_modules/.pnpm-config. A malicious repository could commit a traversal-shaped name such as `../../PWNED` and make `pnpm install` create symlinks outside that directory, even with `--ignore-scripts`. Validate every config dependency and optional subdependency name as a valid npm package name before any path is built from it, reusing the existing assertValidDependencyAliases / is_valid_old_npm_package_name check. The same fix is applied to both the TypeScript CLI and pacquet. Fixes GHSA-qrv3-253h-g69c.
…tions The earlier assertions probed guessed locations rather than where the symlink would actually land. A traversal-shaped config dep name joined onto <rootDir>/node_modules/.pnpm-config normalizes to <rootDir>/PWNED, and an optional subdep escapes into the store links tree. Replace the point checks with a recursive search (that does not follow symlinks) over both the project and the store directories, so a reintroduced escape to any of those locations is caught.
…cumulators
The name-validation gate collected config dependency and optional subdependency
names into plain `{}` objects before validating their `Object.keys`. A name of
`__proto__` set the accumulator's prototype instead of becoming an enumerable
own key, so the validator never saw it and the invalid name slipped past the
gate — the optional subdep then reached the install/fetch step.
Build those accumulators (and the `normalizeFromLockfile` / migration result
maps) with `Object.create(null)`, so a `__proto__` name lands as an own key the
validator rejects with ERR_PNPM_INVALID_DEPENDENCY_NAME. Added regression tests
on both stacks; pacquet's string-keyed maps have no prototype semantics and
already rejected it, so the Rust test just pins that parity.
… lockfile On the legacy manifest-migration path, names were normalized — which writes pnpm-lock.yaml and workspace settings to disk — before the validation gate ran, so a malicious name could trigger those writes and only then be rejected. Validate the raw manifest/lockfile config dependency names up front, before any normalization or lockfile write, so an invalid name is refused with no side effects. Applied to both stacks (TS `installConfigDeps`, pacquet `resolve_and_install_config_deps`), with regression tests asserting no pnpm-lock.yaml is written. Also added the missing no-artifact assertions to the optional-subdep `__proto__` test for consistency.
…ckfile A config dependency's `version` is also a global-virtual-store path segment (`<name>/<version>/<hash>`), so a committed lockfile with a traversal-shaped version such as `../../../PWNED` could escape the store links root and write package files outside it during install — the name-only validation didn't cover this. Validate every config dependency and optional subdependency version as an exact semver version at the same chokepoint that validates names, before any path is built. Applied to both stacks (TS `installConfigDeps`, pacquet `install_config_deps`) with regression tests for traversal-shaped parent and subdep versions; verified each fails without the fix.
…the lockfile The version-traversal fix validated versions only after normalization, but the legacy manifest-migration path writes pnpm-lock.yaml (and pacquet records lockfile entries and writes them) before that gate ran — so a traversal-shaped version extracted from the inline `<version>+<integrity>` form could be persisted before being rejected. Validate the migrated version as exact semver inside the migration, before any lockfile or settings write. The TS version check moved to a shared `assertValidConfigDepVersion` module so both `installConfigDeps` and `migrateConfigDepsToLockfile` use it without a circular import; pacquet's `assert_valid_config_dep_version` is now called from the migrate branches too. Regression tests on both stacks assert no pnpm-lock.yaml is written for an invalid migrated version; verified each fails without the fix.
…ssage Lock the rendered `InvalidConfigDepVersion` message with an exact-string assertion. The version is wrapped in single quotes (`... invalid version "<version>"`); the assertion guards against a doubled or dropped quote so the diagnostic stays well-formed.
Remove comments that restated the code or narrated test scenarios (tests are documentation), drop call-site comments that duplicated the callee's doc, and collapse the repeated security rationale to a single concise note per validator. Keep only the genuinely non-obvious "why": the store-path-segment reason for validating names/versions, the null-prototype `__proto__` gotcha, the symlink-skip in the search helper, the JSON.parse-own-key test mechanic, and the pre-write ordering.
…yEnvLockfile Following the convention of the main lockfile's verifyLockfileResolutions (whose always-on offline checks already validate dependency aliases), introduce a single verifyEnvLockfile / verify_env_lockfile that validates every config-dependency and optional-subdependency name and version on the env lockfile before any path is built or written. This replaces the scattered per-call-site checks (assertValidRawConfigDepNames, assertValidConfigDeps, the migrate-path version checks, and the pacquet equivalents) with one named gate invoked where the env lockfile materializes or just before each write — which also keeps the "no pre-rejection write" guarantee structurally. Behavior is unchanged; the existing traversal/__proto__/version and pre-write regression tests pass on both stacks.
…ied-write seam The pre-write verifyEnvLockfile calls were repeated at every persistence site. Add a single writeVerifiedEnvLockfile / write_verified_env_lockfile that verifies then writes, and route every env-lockfile write through it. This removes the four scattered pre-write checks, makes "an env lockfile is never persisted unverified" a structural guarantee rather than a convention, and closes a gap where resolvePackageManagerIntegrities wrote without verifying. Only two explicit verifyEnvLockfile calls remain — at the install boundary, where a committed lockfile is installed without being rewritten (a genuinely distinct trust point from persistence). Behavior is unchanged; all tests pass on both stacks.
The note explained that the manifest write doesn't run when writeVerifiedEnvLockfile throws — which is just how `await` works. The function name already says it verifies.
|
Code review by qodo was updated up to the latest commit 863b24c |
Trim the doc comments on the env-lockfile verifier, the verified-write seam, the version validator, and the related tests down to the non-obvious why, and drop a call-site comment that only restated `await` sequencing.
863b24c to
1b9a673
Compare
|
Code review by qodo was updated up to the latest commit 1b9a673 |
…12504) Backports three merged security fixes from main to release/10: - Contain hoisted dependency aliases so a crafted lockfile alias cannot escape node_modules or overwrite pnpm-owned layout. The hoisted graph builder now validates each alias at the safeJoinModulesDir sink. (GHSA-fr4h-3cph-29xv, #12343) - Contain pnpm patch-remove deletions to the configured patches directory. (#12341) - Reject path-traversal config dependency names and versions from pnpm-workspace.yaml before they are used to build filesystem paths. (GHSA-qrv3-253h-g69c, #12470) Written by an agent (Claude Code, claude-opus-4-8).
Summary
Fixes the path-traversal vulnerability reported in GHSA-qrv3-253h-g69c (CWE-22, High).
Config dependency names and versions are read from the committed env lockfile (
pnpm-lock.yaml) and the legacy inline-integrity format inpnpm-workspace.yaml. Both become path segments of the directories pnpm creates during install —node_modules/.pnpm-config/<name>and the global virtual store's<name>/<version>/<hash>— but they were used unvalidated. A malicious repository could commit a traversal-shaped name (../../PWNED) or version (../../../PWNED) and makepnpm installcreate symlinks or write package files outside those roots, triggered on install even with--ignore-scripts.Fix
Add an offline structural gate,
verifyEnvLockfile(verify_env_lockfilein pacquet), that validates every config-dependency and optional-subdependency name (must be a valid npm package name) and version (must be an exact semver version) before any path is built from it. It runs at the install boundary and, through a singlewriteVerifiedEnvLockfileseam, before the env lockfile is ever persisted — so an invalid entry is rejected with no write side effect.__proto__names are rejected too (the validation accumulators use null-prototype objects so the key can't slip pastObject.keys). The same fix and structure land in pacquet to keep the two stacks in sync.Tests
installing/env-installer) and pacquet (crates/env-installer): regression tests covering traversal-shaped names and versions,__proto__, and the legacy/workspace migration path — asserting rejection, no escaped entry under the project or store, and nopnpm-lock.yamlwrite on invalid input.All env-installer tests pass on both stacks; lint, cspell, rustfmt, taplo, clippy, and dylint are clean.
Squashed commit body
Written by an agent (Claude Code, claude-opus-4-8).
Summary by CodeRabbit
Release Notes
Bug Fixes
Tests
__proto__cases, and legacy/workspace migration scenarios—verifying no escaped entries or unintendedpnpm-lock.yamlwrites occur.