Your team publishes an internal npm package to an Azure Artifacts feed that also has npmjs.com configured as an upstream source. How can a build still end up installing a public package of the same name, and what prevents it?
answer
- feed-first is per version, not per name
- floating range is the opening
- scope the name you own
- one source in the client config
- lockfile plus exact pin closes it
basics
~20 sResolution is per version, not per name: if a client asks for a version your feed does not hold, the feed will fetch it from the public upstream. A floating range plus a higher public version is the opening. Scoped names, exact pinning and a single configured source close it.
solid answer
~50 sAzure Artifacts resolves feed-first, but at the level of a **version**, not a package name. Anything already in the feed is served locally; anything not there is looked for in the upstream sources. So if your internal `payments-sdk` is at `1.4.0` and the client asks for `^1.0.0`, an attacker who publishes `payments-sdk 9.9.9` to npmjs.com has supplied a version your feed cannot satisfy — and the feed will happily save and serve it. That is the substitution risk inside a feed that has a public upstream. The defences are ordinary but must be deliberate: publish internal packages under a registry **scope** (`@contoso/payments-sdk`) so the name is not claimable publicly, map that scope to the feed, keep **one** source in the client config so nothing can compare versions across registries, pin exact versions with a committed lockfile, and restrict who holds Collaborator on the feed so arbitrary upstream saves are not everyone's privilege.
code
ini · 13 lines; nuget.config — internal IDs may come only from the private feed
<configuration>
<packageSources>
<clear />
<add key="internal" value="https://pkgs.dev.azure.com/contoso/tools/_packaging/shared/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="internal">
<package pattern="Contoso.*" />
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>go deeper
Understand that a package name on a public registry is claimable by anyone, and that installing by a loose version range lets a stranger's package satisfy your dependency.
Explain that the feed resolves per version rather than per name, and describe the concrete controls: scoped names, one configured source, exact pins with a lockfile honoured in CI.
Walk the failure end to end, name which control blocks which step, and reason about the Collaborator role and the feed package list as the privilege and detection layers around it.
Own the organization-wide policy: which name patterns are reserved, whether every team may configure upstreams, how new dependencies are reviewed, and what evidence you would produce after a substitution incident.
## Why the risk survives a private feed A private feed with an upstream source is usually sold as the safe arrangement, and it is far safer than listing two registries in the client config. But it is not a name-ownership boundary. Understanding exactly what the feed guarantees is the whole question. The guarantee is: **a version present in the feed is served from the feed.** Upstream is consulted only for versions the feed does not hold. That is version-level resolution, and the gap it leaves is a version your feed has never seen. ## The concrete sequence 1. Your team publishes `payments-sdk` versions `1.0.0` through `1.4.0` to the feed. Nothing with that name exists on npmjs.com. 2. A `package.json` somewhere declares `"payments-sdk": "^1.0.0"`, or a CI job runs an update that widens the range, or someone installs without a lockfile. 3. Someone registers `payments-sdk` on npmjs.com and publishes `9.9.9`. 4. A client resolving the range asks the feed for the highest matching version. `9.9.9` is not in the feed, so the upstream is consulted, the public package is found, saved into the feed, and installed — with its install scripts. No step here is a bug. Every component did what it was configured to do. This is the substitution class of supply-chain attack, and the feed's upstream is the delivery path. ## The defences, ordered by how much they actually buy **Use a registry scope for internal packages.** In npm, publish as `@contoso/payments-sdk` and map the scope to the feed in `.npmrc`: ```ini @contoso:registry=https://pkgs.dev.azure.com/contoso/tools/_packaging/shared/npm/registry/ //pkgs.dev.azure.com/contoso/tools/_packaging/shared/npm/registry/:_authToken=${NPM_TOKEN} ``` A scope you control on the public registry cannot be squatted, and scope-to-registry mapping is a routing rule the client honours before any version comparison happens. This is the strongest single control for npm. **Keep exactly one source in the client config.** The worst arrangement is a client configured with both the private feed and the public registry as peers — most clients then resolve across both and take the highest satisfying version, with no notion that one source is more trustworthy. One feed with upstreams configured *server-side* removes the comparison entirely: the client only ever talks to one endpoint, and the ordering decision lives in feed settings where it can be reviewed. **Pin, and commit the lockfile.** A pinned exact version that exists in the feed never reaches the upstream path at all. A committed `package-lock.json` (or equivalent) plus a CI install that honours it — `npm ci` rather than `npm install` — closes the floating-range window that the attack depends on. This is the control that most often already exists and most often is disabled in CI by accident. **For NuGet, add source mapping.** NuGet clients from 6.0 onwards support `<packageSourceMapping>` in `nuget.config`, which declares which ID patterns may come from which source. Internal prefixes are mapped to the feed and nothing else, so an ID that matches an internal pattern can never be satisfied elsewhere regardless of version. **Restrict who can save from upstream.** Saving a new version from an upstream requires at least the Collaborator role on the feed. Handing Collaborator to everything by default means any identity can pull arbitrary public packages into the shared feed. Reserving it narrows who can widen your dependency surface. **Reserve the name publicly.** Publishing a placeholder under the internal name on the public registry is a blunt but real mitigation for unscoped ecosystems where nothing else fits. ## What to say about detection Because upstream saves land in the feed as ordinary packages, the feed's package list is a genuine inventory of everything ever pulled from public registries. Reviewing that list — particularly new entries with names that look internal — is a cheap detection layer that costs nothing to set up and is the reason a single feed with upstreams beats letting every build reach the public registry directly. ## The honest summary for an interview The feed gives you one endpoint, one audit trail and version-level precedence for what you have already published. It does not give you name ownership. The name is claimed by scoping it, and the version is pinned by a lockfile; the feed is the place those two controls are enforced consistently, not a substitute for them.
- Why is listing the private feed and the public registry as two sources in the client config worse than one feed with an upstream?With two peer sources the client compares versions across both and takes the highest match, so a public package can outrank yours by version number alone. With one feed the client has nothing to compare — precedence is decided server-side in feed settings, where it is configured once and can be reviewed.
- Does a committed lockfile fully solve this?For existing dependencies, largely yes — a pinned exact version is served from the feed and never reaches upstream. It does not cover the first resolution of a newly added dependency, a deliberate range widening, or a CI job that runs an installer which ignores the lockfile, so it is a strong layer rather than the whole answer.
- What makes an npm scope stronger protection than a naming convention like internal-payments-sdk?A convention is only a string an attacker can also register publicly. A scope you own on the public registry cannot be claimed by anyone else, and scope-to-registry mapping routes those requests to your feed before any version comparison happens — it is enforcement in the client rather than a hope about names.
- How would you find out whether this has already happened to your feed?Review the feed's package list. Every version saved from an upstream is stored in the feed as an ordinary package, so the list is a complete inventory of what your organization pulled from public registries. Entries whose names match internal patterns but arrived from upstream are exactly the signal you are looking for.
saying these in an interview costs you the question
- Assuming a private feed makes package names unclaimable
- Listing both the feed and the public registry as client sources
- Thinking upstream sources scan or vet what they save
- Relying on a naming prefix instead of an owned scope
- Running an installer in CI that ignores the committed lockfile