fix(bundle): isolate esbuild downloads from workspace registries - #36467
Conversation
bartlomieju
left a comment
There was a problem hiding this comment.
Direction looks right, and the spec test is a genuine regression test — spec tests set NPM_CONFIG_REGISTRY to the local test registry via add_npm_env_vars(), so the @esbuild:registry=localhost:4545 override in the fixture would have broken this pre-fix. A few things before this lands:
Workspace .npmrc is dropped entirely, not just the scoped override. create_default_npmrc discards a plain unscoped registry=, _authToken, and cert/proxy config too. Air-gapped and corporate setups that mirror all of npm through a project .npmrc (with no NPM_CONFIG_REGISTRY set) will now have deno bundle reach for registry.npmjs.org and fail. I think we want to keep the workspace's default registry and its auth, and only ignore scope-specific overrides like @esbuild:. Right now the fix is broader than the bug.
Trust policy bypass. create_default_npmrc sets trust_policy: Default::default(). If a workspace configured a tarball trust policy, the esbuild tarball now skips it. Intended?
Duplication with NpmInstallerFactory. This hand-rolls the cache dir, cache, http client, registry info provider, and tarball cache that the factory already builds. Worth extracting a helper so the two don't drift as ResolvedNpmRc gains fields — replace_registry_host, min_release_age_days, and trust_policy are all things that could silently diverge here.
Test coverage. The fixture only exercises the scoped @esbuild:registry override. Once the default-registry question above is settled, please add a registry= case to pin that behavior down.
bartlomieju
left a comment
There was a problem hiding this comment.
The problem is real and worth fixing. Resolving the pinned @esbuild/* platform package through the workspace .npmrc means a project being bundled gets to choose where that binary comes from — and Deno then spawns it as a subprocess. So deno bundle on an untrusted checkout was arbitrary code execution via three lines of .npmrc. The test is well built too: pointing @esbuild:registry at the file server (which can't serve packuments) while @denotest:registry points at the private test registry proves isolation in one direction and continued workspace resolution in the other, in a single run.
But I think the isolation is drawn in the wrong place, and as written it breaks a common setup.
create_default_npmrc builds purely from env — NpmRegistryUrl::for_npm(sys) and ReplaceRegistryHost::for_npm(sys). It never reads any .npmrc file. The old path went through factory.npmrc(), which discovers both the project .npmrc and the user's ~/.npmrc and merges them.
So this doesn't just drop the project config — it drops the user's home config too. Anyone whose registry mirror lives in ~/.npmrc (registry=https://nexus.corp/repository/npm-group/), which is how corporate mirrors are normally configured, currently gets esbuild from that mirror and after this change would have Deno reach for registry.npmjs.org instead. On an air-gapped or egress-filtered network deno bundle stops working entirely, with a network error that gives no hint that a config file was ignored.
The trust boundary that matters is project-vs-user, not file-vs-env: ~/.npmrc is the user's own configuration, exactly as trusted as NPM_CONFIG_REGISTRY, while the project .npmrc is the attacker-controlled input in the threat model above. And the code already has that split — merge_npm_rc(project_rc, home_rc) in libs/resolver/npmrc.rs takes them as separate arguments. Resolving from the home rc alone and skipping the project rc would keep the security property while leaving mirrored setups working.
Second, smaller point: this hand-assembles NpmCacheDir, CliNpmCache, CliNpmCacheHttpClient, CliNpmRegistryInfoProvider and TarballCache inline — about forty lines reproducing what CliFactory/npm_installer_factory already build. If any of those gain a parameter or a behavior the factory sets up, this copy silently diverges and the divergence shows up as a bundler-only bug. Worth hanging it off the factory as something like factory.isolated_npm_client() so there's one construction site.
Happy to re-review. The direction is right; I just don't want mirrored installs to become collateral.
…oland#36467) ## Summary - give the bundler's pinned esbuild helper an npm client that is independent of workspace `.npmrc` settings - preserve explicit process-wide registry mirrors and normal cache/reload behavior - keep workspace registry configuration active for project package resolution ## Background The bundler accidentally reused the workspace npm registry provider and tarball cache when acquiring its internally selected platform-specific esbuild binary. This made helper resolution depend on package settings from the project being bundled. Constructing a dedicated default npm client keeps helper acquisition deterministic while project dependencies continue to resolve through workspace settings. ## Tests - `cargo check -p deno --bin deno` - `cargo clippy -p deno --bin deno --no-deps -- -D warnings` - `cargo test -p specs_tests --test specs -- specs::bundle::`
) ## Summary - give the bundler's pinned esbuild helper an npm client that is independent of workspace `.npmrc` settings - preserve explicit process-wide registry mirrors and normal cache/reload behavior - keep workspace registry configuration active for project package resolution ## Background The bundler accidentally reused the workspace npm registry provider and tarball cache when acquiring its internally selected platform-specific esbuild binary. This made helper resolution depend on package settings from the project being bundled. Constructing a dedicated default npm client keeps helper acquisition deterministic while project dependencies continue to resolve through workspace settings. ## Tests - `cargo check -p deno --bin deno` - `cargo clippy -p deno --bin deno --no-deps -- -D warnings` - `cargo test -p specs_tests --test specs -- specs::bundle::`
Summary
.npmrcsettingsBackground
The bundler accidentally reused the workspace npm registry provider and tarball cache when acquiring its internally selected platform-specific esbuild binary. This made helper resolution depend on package settings from the project being bundled.
Constructing a dedicated default npm client keeps helper acquisition deterministic while project dependencies continue to resolve through workspace settings.
Tests
cargo check -p deno --bin denocargo clippy -p deno --bin deno --no-deps -- -D warningscargo test -p specs_tests --test specs -- specs::bundle::