RoSoft All articles
Downloads & Tools

When an Internal Name Resolves Outside: Auditing Registry Precedence in 2026

RoSoft
When an Internal Name Resolves Outside: Auditing Registry Precedence in 2026

Photo: DFID - UK Department for International Development, CC BY 2.0, via Wikimedia Commons

Most supply-chain incidents in package management are described as attacks, but the mechanism is duller than that. A build tool is asked for a name. It looks in more than one place. It finds a version somewhere it should not have looked, and it installs it. The rest is consequence. For teams that maintain private packages, the useful framing for 2026 is not "how do we detect malicious code" but "how do we prove, on paper and in CI, where each name is allowed to come from."

The failure mode, stated plainly

Every major package manager supports multiple sources: a default public registry, one or more internal feeds, sometimes a mirror or proxy in between. Dependency confusion occurs when an internal package name is either unscoped or scoped without pinning, and the resolver is permitted to consult the public registry for that name. If the private feed lacks the package on a given machine or pipeline, the public result wins. An attacker who learns the name only needs to publish it upstream at a higher version number.

The names most at risk are the ones nobody thinks of as names: build helpers, internal SDKs, shared lint configs, test fixtures published years ago and forgotten. They tend to be unscoped, because scoping felt like ceremony at the time.

Step one: build a name inventory you can defend

An inventory is not a spreadsheet of repositories. It is a list of package identifiers as the resolver sees them, per ecosystem, with an owner and a publication status.

Flag every unscoped internal name as a finding, even if it currently resolves correctly. Correct today is not the same as constrained.

Step two: write down the intended resolution order

Undocumented precedence is the root cause. Document it per ecosystem and keep the document next to the configuration it describes, not in a wiki that drifts.

npm and compatible registries. A project-level .npmrc should map scopes explicitly: @yourco:registry=https://registry.yourco.internal/. Set the default registry= to your proxy if you run one. Remember that configuration layers: command line beats environment, which beats project, which beats user, which beats global. An engineer's personal ~/.npmrc can silently override the project. Audit both.

Python. The single most common trap is --extra-index-url. It does not mean "try this second"; pip merges candidate versions from all configured indexes and selects the highest. If your private index is listed as an extra index, a public package with a higher version wins. Point --index-url at an internal proxy that itself proxies public packages, or accept the risk knowingly.

NuGet. Use packageSourceMapping in nuget.config to bind package ID patterns to specific sources. This is the closest thing to a firewall that the ecosystem offers, and it is underused.

Maven and Gradle. Repository order in settings.xml and build files matters, and mirrors can invert it. mirrorOf declarations deserve the same review as a firewall rule.

Step three: make precedence enforceable in CI

A documented order that nobody tests is a wish. Add a registry lint job that runs on every pull request:

  1. Parse the effective configuration (npm config list --location=project, pip config debug, dotnet nuget list source) and fail if an internal scope is not mapped to an internal host.
  2. Fail if an unscoped internal name appears in a manifest at all, unless it is on a dated exception list.
  3. Attempt a dry-run install with public egress blocked. If the build succeeds, the internal feed is sufficient. If it fails, you have found a machine that was reaching upstream for something it should not have needed.

Blocking egress from build agents is the control that survives configuration mistakes. Everything else is documentation.

Step four: verify what the lockfile actually recorded

Lockfiles are evidence. For npm, inspect the resolved URL of every entry; a host change from your internal registry to the public one is a red flag worth a blocking review rule. NuGet's packages.lock.json and Python hash-pinned requirements serve the same purpose. Enforce npm ci, dotnet restore --locked-mode, and pip install --require-hashes in pipelines so that the lockfile is authoritative rather than advisory.

Run a periodic diff of lockfile registry hosts across the repository fleet. It is a cheap check and it catches the drift that manual review misses.

Step five: rehearse the leak response

Assume that one internal name will eventually appear upstream. Decide in advance who does what:

Time the rehearsal. If containment takes longer than the time to run a build, the procedure needs revision.

A quarterly cadence

Thirty minutes a quarter is sufficient: regenerate the name inventory, review scope mappings and source patterns, rerun the egress-blocked build, and confirm the registry monitoring alert still fires. The goal is not a one-time cleanup but a configuration that fails loudly when it drifts.

Private package names are infrastructure. Treat their resolution paths the way you treat DNS or routing tables: documented, tested, and owned.

All Articles

Related Articles

Reading the Exit Door: Auditing Windows Uninstall Routines Before Rollout

Reading the Exit Door: Auditing Windows Uninstall Routines Before Rollout

A Portable Workshop, Not a Junk Drawer: Structuring a Developer Rescue USB for 2026

A Portable Workshop, Not a Junk Drawer: Structuring a Developer Rescue USB for 2026

The Compiler Does Not Read the README: Synchronizing Toolchains Across Local, CI, and Production in 2026

The Compiler Does Not Read the README: Synchronizing Toolchains Across Local, CI, and Production in 2026