Skip to content

fix(bundle): isolate esbuild downloads from workspace registries - #36467

Merged
nathanwhit merged 2 commits into
denoland:mainfrom
nathanwhit:fix/bundle-esbuild-registry
Aug 12, 2026
Merged

fix(bundle): isolate esbuild downloads from workspace registries#36467
nathanwhit merged 2 commits into
denoland:mainfrom
nathanwhit:fix/bundle-esbuild-registry

Conversation

@nathanwhit

Copy link
Copy Markdown
Member

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::

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 bartlomieju closed this Aug 7, 2026
@bartlomieju bartlomieju reopened this Aug 7, 2026

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nathanwhit
nathanwhit merged commit f169b71 into denoland:main Aug 12, 2026
136 checks passed
gauravtiwari pushed a commit to boringcache/deno that referenced this pull request Aug 13, 2026
…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::`
bartlomieju pushed a commit that referenced this pull request Aug 27, 2026
)

## 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::`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants