Dependency Confusion - npm Namespace Takeover Across 4 Brands
The Risk
Production websites for a major financial institution referenced internal software packages that were never registered on the public package registry. Anyone could have claimed those package names for free, published malicious versions, and had their code automatically pulled into the company's next software build. A single compromised build would have affected all four of the institution's brand websites simultaneously.
The Vulnerability
Production JavaScript bundles served across 4 brand domains referenced 5 internal npm packages under 2 separate organization scopes. Neither scope was registered on the public npm registry. Both were freely claimable by anyone, with no verification required.
How it was found
Static analysis of the production JavaScript bundles revealed internal package names in webpack Module Federation shared dependency declarations and bundled chunk files. Querying the public npm registry for both organization scopes returned 404s, confirming they were unregistered.
What was exposed
The first scope contained 3 packages related to a micro-frontend platform framework (routing, theming, and a core framework module). The second scope contained 2 packages related to an internal design system with brand-specific themes for 7 sub-brands. All 5 packages were referenced with specific version numbers in production bundles.
The same JavaScript bundle (identical hash) was served across all 4 brand domains, meaning a single compromised build would propagate universally.
The Attack
The attack was not executed. Both npm organization scopes were registered defensively to prove exploitability, but zero packages were published. The theoretical attack chain:
- Register both npm organization scopes on npmjs.com (free, instant, no verification required)
- Publish packages matching the internal names with a higher version number (e.g., 1.24.0 vs the referenced 1.23.0)
- Include a
postinstallscript in the package that executes duringnpm install - If the CI/CD pipeline's npm configuration allows public registry fallback for these scopes, npm's version resolution prefers the higher version from the public registry over the lower version from the private registry
- On the next build, the malicious package runs arbitrary code inside the CI/CD runner with access to environment variables, secrets, source code, and deployment credentials
The packages were webpack Module Federation shared dependencies, resolved at build time. This means exploitation happens during the build process, not at runtime in the user's browser.
The Impact
CI/CD code execution
A postinstall script runs with the full privileges of the build process. This typically includes access to environment variables (API keys, deployment tokens), the full source code repository, and the ability to modify build outputs.
Cross-brand compromise
All 4 brand domains served the same build artifact. A single poisoned dependency would affect every brand simultaneously, with no additional effort from the attacker.
Supply chain persistence
Once a malicious package is in the dependency tree, it persists across builds until someone notices and removes it. The malicious code could inject a backdoor into the production JavaScript served to millions of banking customers.
Defensive registration as proof
Both npm organization scopes were successfully registered by the researcher, proving that any attacker could have done the same. The scopes were held defensively and disclosed to the target for transfer.
Remediation
- Register and claim all internal npm organization scopes on the public npm registry, even if packages are only used privately
- Configure
.npmrcwith scope-specific registry mappings to prevent public fallback:@scope:registry=https://internal-registry.example.com - Use npm's
--ignore-scriptsflag in CI/CD to prevent postinstall execution from untrusted packages - Audit production JavaScript bundles for leaked internal package names and remove them from source maps and chunk identifiers
- Implement package integrity verification (lockfile pinning with
npm ci) in all build pipelines