Repository navigation
install: refuse a dependency name that node_modules cannot hold (.bin, node_modules, bare @scope) #44439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
install: refuse a dependency name that node_modules cannot hold (.bin, node_modules, bare @scope) #44439
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -471,7 +471,7 @@ impl ExtractTarball { | |
| // The entire body lives inside the thread_local borrow closure. | ||
| let folder_name: &[u8] = match self.resolution.tag { | ||
| ResolutionTag::Npm => { | ||
| if !bun_install::dependency::is_safe_install_folder_name(name) { | ||
| if !bun_install::dependency::is_valid_node_modules_entry_name(name) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 pre-existing, not blocking: Pre-existing, security: an untrusted registry package can still get its own Why this was flaggedA registry package Verification: pre-existing. Trigger: an untrusted registry tarball contains |
||
| log.add_error_fmt( | ||
| None, | ||
| bun_ast::Loc::EMPTY, | ||
|
Comment on lines
+474
to
477
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) A registry (private mirror, proxy, or compromised registry) serving a package named Why this was flaggedA dependency Verification: nit — triggered when a dependency's resolved name is refused, e.g. |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -287,9 +287,7 @@ impl<'a, const PATH_STYLE: IteratorPathStyle> Iterator<'a, PATH_STYLE> { | |
| } | ||
| } | ||
|
|
||
| /// Tree folder names are joined into install destinations as | ||
| /// `node_modules/<name>/...`; this path and the tree builder must agree on the | ||
| /// same validator. | ||
| /// A tree folder name is joined onto `node_modules/`, so it has to stay inside it. | ||
| pub(crate) fn folder_name_is_safe(name: &[u8]) -> bool { | ||
| crate::dependency::is_safe_install_folder_name(name) | ||
| } | ||
|
|
@@ -748,14 +746,17 @@ impl Tree { | |
| .name | ||
| .slice(lockfile.buffers.string_bytes.as_slice()); | ||
| if !dependency_name.is_empty() | ||
| && !crate::dependency::is_safe_install_folder_name(dependency_name) | ||
| && !crate::dependency::is_valid_node_modules_entry_name(dependency_name) | ||
| { | ||
| builder.maybe_report_error(format_args!( | ||
| "Invalid dependency name \"{}\"", | ||
| dependency | ||
| .name | ||
| .fmt(lockfile.buffers.string_bytes.as_slice()), | ||
| )); | ||
| // An unresolved dependency gets no folder, and the resolver has reported it. | ||
| if pkg_id != invalid_package_id { | ||
|
Comment on lines
748
to
+752
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 (optional) Users with a refused dependency key that did not resolve now get no Why this was flaggedpackage.json holds Verification: Triggered when a refused key ( |
||
| builder.maybe_report_error(format_args!( | ||
| "Invalid dependency name \"{}\"", | ||
| dependency | ||
| .name | ||
| .fmt(lockfile.buffers.string_bytes.as_slice()), | ||
| )); | ||
| } | ||
| continue 'dep; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3262,7 +3262,9 @@ pub(crate) fn parse_into_binary_lockfile( | |
| .is_none_or(|manager| manager.options.link_workspace_packages), | ||
| ); | ||
|
|
||
| if let Err(tree::SubtreeError::OutOfMemory) = lockfile.resolve(log) { | ||
| // The install reports a refused name itself, after it applies package.json. | ||
| let mut refused_names = bun_ast::Log::init(); | ||
| if let Err(tree::SubtreeError::OutOfMemory) = lockfile.resolve(&mut refused_names) { | ||
|
Comment on lines
+3265
to
+3267
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 Users migrating a package-lock.json or pnpm-lock.yaml whose root entry still holds a name the new rule refuses, after package.json dropped it, get a full install, then exit 1 and no bun.lock, on every run. Only the bun.lock loader got the throwaway log; migration.rs:406 and pnpm.rs:1621 still pass the shared log to resolve, so install_with_manager.rs:658 records the stale error and :926 crashes after the linker and lifecycle scripts ran. Fix: every lockfile loader that rebuilds the tree keeps the name report to itself, as bun.lock.rs:3267 does, so only the re-check in clean_with_logger decides; names package.json still holds must keep failing at :689. Why this was flaggedTrigger: a project with no bun.lock but a package-lock.json (or pnpm-lock.yaml) whose root entry still declares a key the new predicate refuses but npm/pnpm accept, e.g. "@ acme/node_modules", while package.json no longer has it. Entry point: this.resolve(log) at src/install/migration.rs:406 (pnpm: src/install/pnpm.rs:1621). The tree builder at src/install/lockfile/Tree.rs:753 adds "Invalid dependency name" to the shared log; report_migrated (migration.rs:237) only resets the log when it has no errors, so the error survives. In install_with_manager.rs:658 had_errors_before_cleaning_lockfile becomes true. The diff and clean drop the stale dependency, so the new gate at :689 does not fire; the hoisted or isolated linker runs, lifecycle scripts run, then :926 crashes on the stale flag: exit 1, no bun.lock written. The PR fixed exactly this recovery problem for bun.lock (throwaway Log at bun.lock.rs:3266-3267) but not for the two migration loaders, which call the same resolve(). On the base branch these names were accepted, so the migration succeeded. Verification: The migration loaders hand the shared log to the tree builder: |
||
| return Err(ParseError::OutOfMemory); | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 pre-existing, not blocking: On a case-insensitive macOS volume a registry package can still land its payload at
node_modules/node_modulesand have a trusted postinstall run itsnode. The new gate at src/install/dependency.rs:572 compares the package half witheq_ignore_ascii_case, but APFS folds Unicode too: the keynode_moduleſ(U+017F long s) passes the check, yet the filesystem treatsnode_modules/node_moduleſ/.binasnode_modules/node_modules/.bin, which src/install/PackageManager/PackageManagerLifecycle.rs:386 puts on every lifecycle script's PATH. Fix: compare the package half againstnode_modulesafter Unicode simple case folding, not ASCII folding only.A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
A registry package
carrierdeclares"node_moduleſ": "npm:payload@ 1.0.0"where the last character is U+017F;payloadships.bin/node. The project installscarrierand a trusted package with anode-invoking postinstall on macOS, whose default APFS volume is case-insensitive and folds names with Unicode CaseFolding.is_valid_node_modules_entry_nameat src/install/dependency.rs:560-572 takespackage = name, andpackage.eq_ignore_ascii_case(b"node_modules")is false, so the name is accepted by the tree builder (src/install/lockfile/Tree.rs:749) and the hoisted alias gate. PackageManagerLifecycle.rs:383-390 appends<dir>/node_modules/.binfor every ancestor of the script cwd, so a script innode_modules/nativehas<project>/node_modules/node_modules/.binon PATH; the APFS lookup matches the foldednode_moduleſentry, andnoderesolves to the payload. The base branch has the same exposure for the plainnode_moduleskey, which this PR closes; the stated case-insensitivity defense (comment at dependency.rs:568) does not hold on the volumes it targets.Verification: src/install/dependency.rs:572 checks
!package.eq_ignore_ascii_case(b"node_modules"), which only folds ASCII, sonode_moduleſ(U+017F) passes and is accepted by Tree.rs:749, PackageInstaller.rs:368-370 and extract_tarball.rs:474. PackageManagerLifecycle.rs:381-390 puts<dir>/node_modules/.binfor every ancestor of the script cwd on PATH; APFS folds U+017F tos, so the payload'snoderuns.