fix(guards): refuse a .npmrc beside a portal manifest, like bunfig.toml

Bun reads .npmrc too, so a registry line beside a borrowed portal's
manifest fetches every dependency from a server the portal's author
picked - the same door bunfig.toml opens, which the installer and guard
check 3 already refuse. Both now reject either file. The #718 integration
test, which pointed Bun at its offline registry through a .npmrc, passes
the registry through NPM_CONFIG_REGISTRY instead; it still proves the
hook stays off, because the hooked package only exists on that server.

Follows GHSA-wq7j-x5jq-pjrw; this case is ours, not the report's. Guard
tests for the workspaces and bunfig.toml refusals, which the previous
commit added without them, come along.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mads LorentzenandClaude Fable 5.1 authored and Mads Lorentzen committed 2026-10-27 16:14:35 +01:00
1 parent 0bcfd75e60
commit 83c788d779
9 files changed
+67 -28

No files matched your search

+7
View File
@@ -83,6 +83,13 @@ class InstallPortalClis(unittest.TestCase):
(cli / "bunfig.toml").write_text('[install]\nregistry = "https://registry.example"\n', encoding="utf-8")
self.assertIn("bunfig.toml", unsafe_reason(cli))
def test_npmrc_is_unsafe(self):
# Bun reads .npmrc too: a registry line beside the manifest fetches
# every dependency from a server the portal's author picked.
cli = self.add_cli("x")
(cli / ".npmrc").write_text("registry=https://registry.example/\n", encoding="utf-8")
self.assertIn(".npmrc", unsafe_reason(cli))
def test_bun_install_never_runs_lifecycle_scripts(self):
# #718: --ignore-scripts on every install, so a dependency's hook stays
# off whatever the Bun release's default trust list says.
+4 -2
View File
@@ -72,10 +72,12 @@ class DependencyHooks(unittest.TestCase):
(cli / "package.json").write_text(json.dumps({
"name": "example-search", "private": True, "dependencies": {"esbuild": "0.25.0"},
}), encoding="utf-8")
(cli / ".npmrc").write_text(f"registry=http://127.0.0.1:{server.server_port}/\n", encoding="utf-8")
# The registry goes in through the environment: a .npmrc beside
# the manifest is one of the things the installer now refuses.
env = {"PATH": str(Path(BUN).absolute().parent) + os.pathsep + os.defpath,
"HOME": str(root), "TMPDIR": str(root), "TEMP": str(root), "TMP": str(root),
"BUN_INSTALL_CACHE_DIR": str(root / "cache")}
"BUN_INSTALL_CACHE_DIR": str(root / "cache"),
"NPM_CONFIG_REGISTRY": f"http://127.0.0.1:{server.server_port}/"}
if "SYSTEMROOT" in os.environ:
env["SYSTEMROOT"] = os.environ["SYSTEMROOT"]
proc = subprocess.run([sys.executable, str(tools / "install_portal_clis.py")],
+24
View File
@@ -874,6 +874,30 @@ class ManifestGuardTests(GuardRepoFixture):
self.assertEqual(result.returncode, 1)
self.assertIn("trustedDependencies", result.stdout)
# GHSA-wq7j-x5jq-pjrw: the other doors into `bun install`. Workspace
# packages carry scripts this check never reads; a config file beside the
# manifest (bunfig.toml, .npmrc) can point every dependency at another
# registry. The installer refuses the same things.
def test_workspaces_fails(self):
self.write_manifest({"name": "example-cli", "workspaces": ["packages/*"]})
result = run_guards(self.root)
self.assertEqual(result.returncode, 1)
self.assertIn("workspaces", result.stdout)
def test_install_config_file_beside_manifest_fails(self):
for name, body in (("bunfig.toml", '[install]\nregistry = "https://registry.example/"\n'),
(".npmrc", "registry=https://registry.example/\n")):
with self.subTest(file=name):
config = self.manifest.parent / name
config.write_text(body, encoding="utf-8")
try:
result = run_guards(self.root)
finally:
config.unlink()
self.assertEqual(result.returncode, 1, result.stdout)
self.assertIn(name, result.stdout)
self.assertIn("example-search/cli/" + name, result.stdout.replace("\\", "/"))
def test_malformed_manifest_shape_fails_cleanly(self):
for data, message in [
([], "top-level JSON value must be an object"),