Unclaimed Package Scope on Live Payment Bundle and Venue Kiosks
The Risk
The company's partner web application referenced internal code modules by names that nobody owned on the public software registry. Anyone in the world could register those names and have their code pulled into the company's payment-card collection screens, into the physical kiosks at partner venues, and into the build pipeline that holds production deploy credentials. The only attacker cost was a free registry account and a single publish.
The Vulnerability
A public source map on the partner portal leaked the application's package.json. It declared dependencies on an internal package scope (referred to here as the company's package scope), plus two unscoped wildcard packages named workspace-lint and workspace-scripts. At the time of testing, every one of those names was unclaimed on the public registry.
Grep across the source maps proved the names were live, not vestigial:
- A package named
<scope>/payments-sdkwas imported by the card-element wrapper used in the partner checkout. Despite the name, the wrapper was consumed by every payment processor adapter the company supported, so a single hijack would reach card collection across the whole payment stack. - A package named
<scope>/kiosk-webviewwas imported by the kiosk module deployed on physical hardware at partner venues. - The two unscoped wildcard names were invoked by the build pipeline's lint step, which ran alongside release-uploader tokens, monitoring API keys, production end-to-end test credentials, and registry publish capability.
- The
postinstalllifecycle was non-empty, so an attacker package would run install-time scripts in the install context with full environment variable access.
Three independent attack paths were claimable from the leak: registering the scope, publishing the unscoped wildcards (which cannot be protected by a scope-to-registry mapping), and the install-script path that benefited from either of the first two.
The Attack
- Open the partner portal in any browser, view the source-map index, and download the source map for the chunk that contained
package.json. - Parse out the dependency list and identify every internal-looking name.
- Probe each name on the public registry. At test time, every one returned
404, confirming the scope and unscoped names were unclaimed. - Register the matching username on the public registry (free, instant) to lock the scope, then publish the unscoped wildcard names with a single command from any account.
- If the company's build pipeline had public-registry fallback for the scope or wildcards, install-time scripts in the attacker package would execute in the build environment on the next deploy.
The researcher took non-destructive defensive action while the report was in triage: reserved the matching username and published inert stubs with no install scripts and an empty exports object, deprecated with a loud warning. Ownership transferable to the company on request.
The Impact
- Every payment processor adapter at once. The card-element wrapper was imported by adapters for many separate payment processors plus the company's own and a delegated adapter. A single hijack reached the card-data collection path for every processor at the same time, the equivalent of a skimmer across the entire payment stack. Card-data regulatory scope.
- Physical venue kiosks. The kiosk module imported the second hijackable package. Attacker code would run on hardware sitting at partner venue entrances.
- Build pipeline. The wildcard names ran during the lint step in CI, alongside release-uploader tokens, monitoring keys, production credentials, registry publish capability, and cloud deploy credentials.
- Developer workstations. Non-empty
postinstallplus a huskypreparescript meant install-time code in attacker packages would also execute on individual developer machines. - Customer data validation. An additional package was imported in customer-form code paths handling national identity validation.
Remediation
- Configure private registry mapping for the internal scope in every CI and developer environment, and block public-registry fallback at the proxy or registry-manager layer.
- Replace unscoped private package names (
workspace-lint,workspace-scripts) with scoped equivalents under the private scope so the registry mapping covers them. - Pin every dependency in the lockfile and verify the
resolvedURLs point only to the private registry. - Add a build gate that fails if any internal-scope or workspace package resolves to the public registry host.
- Strip embedded source content from production source maps, or disable public source maps entirely and upload them only to error-tracking services.
- Disable install-time lifecycle scripts in CI installs where possible (
npm ci --ignore-scripts) and run any required scripts explicitly afterward. - Take ownership transfer of the defensively-claimed username and stub packages.