---
key: "dependency-confusion"
title: "Stop dependency confusion and typosquatting at install time"
url: "https://softwaresecurity.com/domains/supply-chain/dependencies/dependency-confusion/"
api: "https://softwaresecurity.com/api/v1/articles/dependency-confusion.json"
domain: "supply-chain"
topic: "Software supply chain"
type: "Guide"
identifiers: ["CWE-427","CWE-494","CWE-829","CWE-1104","CWE-1357","A03:2025","A08:2025","LLM04:2026","V15.2.4","V15.1.2","CAPEC-471","CAPEC-538"]
tags: ["Dependency management","Supply chain attacks","AI-assisted development","JavaScript and Node.js","Python"]
stages: ["build"]
roles: ["developer","appsec-engineer","platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Stop dependency confusion and typosquatting at install time

Pin every internal package name to its real source, verify installed bytes against a lockfile, and delay or gate install scripts so a public look-alike cannot replace a private dependency.

Canonical URL: https://softwaresecurity.com/domains/supply-chain/dependencies/dependency-confusion/

## In short

Dependency confusion happens when a resolver chooses a public package over your private one of the same name. Bind every internal name to its real registry (npm scopes and `.npmrc`, pip/uv single index, Maven mirror, NuGet `packageSourceMapping`), install from a lockfile with integrity hashes, and keep install scripts off by default. Add a release-age cooldown and never install an AI-suggested name you have not verified.

- Do map every internal package name to one trusted source; a resolver that sees the name on a public index too will usually take the higher version.
- Do install from a committed lockfile with integrity hashes (`npm ci`, `pip --require-hashes`) so the bytes are fixed, not just the version range.
- Do keep dependency install scripts off by default (npm 12 `allowScripts`, pnpm `allowBuilds`, Yarn `enableScripts`) and approve only what you reviewed.
- Do add a release-age cooldown (npm `min-release-age`, pnpm `minimumReleaseAge`, Renovate/Dependabot) so a malicious version is not installed the minute it is published.
- Don't rely on `--extra-index-url` or a second public mirror for internal names; there is no source priority and the name is leaked to the public index.
- Don't install a package name a model suggested without confirming it exists and is the one you meant.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| npm scoped registry | `@scope:registry=https://host/` in `.npmrc` | Pins every package under `@scope` to one registry; unscoped names still resolve against the default registry. | https://docs.npmjs.com/cli/v12/using-npm/scope |
| npm install scripts (v12) | blocked by default | Dependency `preinstall`/`install`/`postinstall` scripts are skipped unless listed in the `package.json` `allowScripts` field; approve with `npm install-scripts approve`. | https://docs.npmjs.com/cli/v12/commands/npm-install-scripts |
| npm `allow-git` / `allow-remote` (v12) | none | git and tarball-URL dependencies no longer resolve unless set to `all` or `root`. | https://github.com/npm/cli/blob/latest/CHANGELOG.md |
| npm `min-release-age` | `null` (opt in, days) | Added in npm 11.10.0; `min-release-age-exclude` added in 11.17.0. | https://docs.npmjs.com/cli/v12/using-npm/config |
| pip index precedence | no priority; highest version wins | `--index-url` and `--extra-index-url` are searched together; prefer one index. | https://pip.pypa.io/en/stable/cli/pip_install/ |
| pip `--uploaded-prior-to` | off (ISO date or `P3D`) | Datetime added in pip 26.0, duration form (e.g. `P3D`) in 26.1; needs upload-time metadata. | https://pip.pypa.io/en/stable/news/ |
| uv `index-strategy` | first-index | Only the first index that has the name is used; `unsafe-best-match` restores pip-like merging. | https://docs.astral.sh/uv/concepts/indexes/ |
| pnpm minimumReleaseAge | 1440 minutes | Default since pnpm v11; `0` before. `minimumReleaseAgeExclude` lists exempt packages. | https://pnpm.io/settings/dependency-resolution |
| pnpm build scripts | blocked (`strictDepBuilds: true`) | pnpm v10+ does not run dependency build scripts, and since v11 an unreviewed one fails the install; list trusted ones in `allowBuilds`. | https://pnpm.io/settings/build |
| Yarn `npmMinimalAgeGate` | e.g. `1w` | Age gate on the publish date; `enableScripts` defaults to `false`, and `enableHardenedMode` switches on by itself for pull requests to public GitHub repositories. | https://yarnpkg.com/configuration/yarnrc |
| NuGet package source mapping | off until configured | Requires NuGet 6.0 / .NET SDK 6.0.100+; once set, every package including transitive must match a pattern or restore fails (`NU1101`). | https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping |
| Dependabot version-update cooldown | 3 days (default) | Applies to version updates only; security updates open immediately. GHES 3.23. | https://github.blog/changelog/2026-07-14-dependabot-version-updates-introduce-default-package-cooldown/ |

## Verify it

1. Grep every lockfile for your internal names and confirm the resolved URL or source is the private registry, not the public one (`package-lock.json` `resolved`, `uv.lock` `source`, `project.assets.json` libraries).
2. In a throwaway environment, publish a higher version of one internal name to a scratch public index and confirm your configured install still takes the private one.
3. Run `npm ci` (or `pip install --require-hashes`) after tampering with one cached tarball and confirm it fails with an integrity error instead of installing.
4. Install a dependency whose `postinstall` writes a marker file and confirm the marker is absent until you explicitly approve the script.
5. Set a release-age cooldown and confirm a version published minutes ago is skipped in favour of an older one.
6. Run your SCA tool and check that it flags unpinned or unmapped dependencies, run OpenSSF Scorecard's Pinned-Dependencies check for unpinned build dependencies, and re-run both after the fix.

## What you will learn

- Explain how a resolver can pick a public package over a private one of the same name.
- Bind internal names to a trusted source in npm, pip/uv, Maven/Gradle and NuGet.
- Fix installed bytes with lockfiles and integrity hashes, and verify the source in CI.
- Reduce install-time execution and add a release-age cooldown against typosquatting and slopsquatting.

Audience: Developers and AppSec or platform engineers who configure package managers, private registries and CI installs

Before you start:

- Read a package manifest and a lockfile, and tell a version range from a resolved version.
- Know the difference between a public registry, a private registry and a repository-manager proxy or group.
- Have a package manager installed to try the commands (npm/pnpm, pip/uv, Maven/Gradle or dotnet).

Time: About 35 minutes: 11 to read and 24 for the exercise

## Why a public name can outrank your private one

Dependency confusion is a resolution bug, not a code bug. When your build asks for a package by name, the installer looks for that name across every source it knows and then picks a "best" candidate, usually the highest version. If the same name also exists on a public registry, and nothing tells the installer that this name is private, the public copy can win. An attacker who learns an internal package name (from a leaked manifest, an error message, a public fork or a job posting) publishes that name on npmjs.com or PyPI with a very high version number and waits for a build to pull it. MITRE files this under CWE-427, Uncontrolled Search Path Element, whose alternate term is literally "dependency confusion" or "substitution attack."

Typosquatting is the related trick aimed at the human rather than the resolver: the attacker registers a name one keystroke from a real one (`crossenv` for `cross-env`, or a hyphen/underscore swap) and waits for a mistype. The 2017 `crossenv` packages wrapped the real module so installs looked normal while the install script shipped environment variables to the attacker. Slopsquatting is the newest variant and aims at AI coding assistants: models invent plausible but non-existent package names, and an attacker pre-registers them. A 2025 study generated 576,000 code samples across 16 models and found 19.7% of recommended packages did not exist, with 205,474 unique hallucinated names, many of them repeatable enough to squat profitably.

All three share one fix shape. Decide in advance exactly which name may come from which source, install only the bytes you reviewed, and give yourself time and a human gate before brand-new code runs on a build machine. The rest of this guide is that shape, per ecosystem, with the current flags.

## npm and pnpm: scope the name to its registry

The strongest npm control is a scoped name bound to a registry. Publish internal packages under an organization scope such as `@acme`, and in a committed `.npmrc` map `@acme` to your private registry while leaving the default registry public. A scope you own cannot be claimed by anyone else on the public registry, so `@acme/ledger` simply does not exist there to confuse the resolver. Keep the auth token in an environment variable rather than in the file. The unscoped case is weaker: a bare name like `acme-ledger` lives in the same namespace as the public registry, so if your private registry is reached through a repository-manager group that also proxies the public one, the higher public version can still win. Prefer scopes for anything internal.

pnpm reads the same `.npmrc` registry mapping. Both npm 12 and pnpm v10+ also changed a dangerous default: dependency install scripts no longer run automatically. In npm 12 the install skips `preinstall`/`install`/`postinstall` for any dependency not listed in the `package.json` `allowScripts` field, prints the blocked packages, and lets you approve or deny them; pnpm 11 and later fail the install (`strictDepBuilds` defaults to `true` since v11) until you list trusted packages in `allowBuilds`. That matters here because the first thing a malicious package wants is code execution at install time, before you ever import it; /domains/supply-chain/dependencies/package-attack-response/ covers the response when one already ran.

```ini
; .npmrc at the repository root, committed with the code
@acme:registry=https://npm.acme.example/
//npm.acme.example/:_authToken=${NPM_TOKEN}
registry=https://registry.npmjs.org/
min-release-age=3
min-release-age-exclude=@acme/*
```

## npm: approve install scripts deliberately

Treat the install-script allowlist as a reviewed artifact. Run the install once, read what npm blocked, and approve only packages whose build step you understand (native modules such as those with a `binding.gyp` are the common legitimate case). An approval written by `npm install-scripts approve` is pinned to the exact version you reviewed, for example `"@acme/native@2.0.0": true`; a denial is name-only, for example `"acme-telemetry": false`, so it holds for every future version. In CI, add `--strict-allow-scripts` so an unreviewed script fails the build loudly instead of being skipped with a warning. The pre-12 escape hatch `--ignore-scripts` still works for a blunt "run nothing" policy.

npm 12 also flipped `allow-git` and `allow-remote` to `none`, so a transitive `git:` or tarball-URL dependency no longer resolves unless you opt in; tarballs on the configured registry's own host still install. That removes a common way for a malicious transitive edge to pull code from outside the registry entirely.

```bash
# npm 12: dependency install scripts are skipped, then listed at the end.
npm install
npm install-scripts ls                      # what was blocked, and the command it wanted to run
npm install-scripts approve @acme/native    # writes "@acme/native@2.0.0": true to allowScripts
npm install-scripts deny acme-telemetry     # writes "acme-telemetry": false
npm rebuild                                 # runs the scripts that are now approved
```

## pip and uv: one index, not two

pip has no notion of index priority. It searches `--index-url` and every `--extra-index-url` together and installs the highest compatible version, wherever it lives. That is exactly how the 2022 PyTorch nightly compromise worked: a package named `torchtriton` on PyPI shadowed the one on PyTorch's own index because "the PyPI index takes precedence." So do not use `--extra-index-url` to reach a private index for internal names. Point `--index-url` at a single private index that proxies or mirrors PyPI, and resolve everything through it, so there is one ordered source and no merge step.

uv is safer by default: its `first-index` strategy only considers the first index that contains a given name, so an internal package found on your index is never also fetched from PyPI. Pin a package to a named index with `[tool.uv.index]` marked `explicit = true` plus a `[tool.uv.sources]` entry, and uv will refuse to take that name from anywhere else. Avoid the `unsafe-best-match` strategy, which restores pip-style merging and reintroduces the confusion risk.

```bash
# Vulnerable: pip merges candidates from both indexes and installs the
# highest version it can find. A public "acme-billing 99.0.0" beats the
# internal 1.4.2, so the attacker's package wins.
pip install --index-url https://pypi.acme.example/simple \
    --extra-index-url https://pypi.org/simple \
    acme-billing
```

## uv: pin an internal name to an explicit index

The fixed uv configuration below binds `acme-billing` to an internal index and marks that index explicit, so no other package drifts onto it and `acme-billing` is never resolved from PyPI. This is the uv analogue of an npm scope map: the name and its source are decided in the project file, not by whichever index happens to answer first.

For the classic pip workflow, keep the same discipline with requirements and constraints files that pin exact versions, and lean on hash checking (next section) so the resolved bytes are fixed even if an index is swapped.

```toml
# pyproject.toml: acme-billing may come only from the internal index.
# "explicit = true" keeps every other package off that index, and the
# tool.uv.sources pin keeps acme-billing off PyPI.
[[tool.uv.index]]
name = "acme"
url = "https://pypi.acme.example/simple"
explicit = true

[tool.uv.sources]
acme-billing = { index = "acme" }
```

## Install from a lockfile with integrity hashes

Pinning a version range is not enough, because the same version can be served as different bytes by a different source. Pin the bytes. `npm ci` installs strictly from `package-lock.json` and fails with an integrity error if a downloaded tarball does not match the recorded sha512; it also refuses to run if the lockfile and `package.json` disagree. For pip, hash-checking mode (`--require-hashes`) requires every requirement to be pinned with `==` and requires a hash for every dependency, transitive ones included, so an unpinned or unexpected package is rejected before download. Add `--only-binary :all:` when you want to refuse source distributions, which run arbitrary code at build time.

Go one step further in CI and check the source, not just the hash. The lockfile records where each package came from, so a short `jq` (or equivalent) check can fail the build when any internal name resolves from a public host. The snippet below combines strict install, hash-checked pip, and a host allowlist over the npm lockfile. It catches the case where someone's local install quietly repointed a dependency at an unexpected registry.

```bash
# CI: install exactly what the lockfiles record, or fail.
npm ci --strict-allow-scripts
pip install --require-hashes --only-binary :all: -r requirements.txt

# Fail the build if any npm package resolves from a host that is not one of
# the two approved registries.
bad=$(jq -r '.packages | to_entries[]
  | select(.value.resolved and (.value.resolved
      | test("^https://(npm\\.acme\\.example|registry\\.npmjs\\.org)/") | not))
  | "\(.key) \(.value.resolved)"' package-lock.json)
[ -z "$bad" ] || { printf 'Unexpected package source:\n%s\n' "$bad"; exit 1; }
```

## Add a release-age cooldown

Most malicious versions are caught and pulled within hours to days. A release-age cooldown turns that window to your advantage by refusing to install a version until it has existed for a few days. npm has `min-release-age` (a day count in `.npmrc`, added in 11.10.0) with `min-release-age-exclude` for packages you control; pnpm has `minimumReleaseAge` (minutes; default 1440 since v11) with `minimumReleaseAgeExclude`; Yarn has `npmMinimalAgeGate`; bun has `minimumReleaseAge` in `bunfig.toml`. Exclude your own internal scope so your freshly published internal versions are not delayed. The four hours in 2021 when ua-parser-js served malware (/domains/supply-chain/dependencies/case-study-event-stream/) are the kind of window a cooldown closes.

If a bot opens your dependency updates, set the cooldown there too. Renovate ships a `security:minimumReleaseAgeNpm` preset that waits three days (it also notes npm's own 72-hour unpublish window), and in 2026 Dependabot made a three-day cooldown the default for version updates while still opening security updates immediately. A cooldown is a delay, not a scanner, so keep it alongside hashes and an allowlist rather than in place of them.

```yaml
# pnpm-workspace.yaml (pnpm 11 or later)
minimumReleaseAge: 4320        # minutes (3 days); the default since v11 is 1440
minimumReleaseAgeExclude:
  - '@acme/*'                  # internal packages are not delayed
allowBuilds:
  '@acme/native': true         # only this dependency may run a build script
strictDepBuilds: true          # the default: an unreviewed build script fails the install
```

## Maven, Gradle and NuGet: map names to sources

The JVM and .NET ecosystems have the same resolution model and the same fix. For Maven, route everything through one repository manager with a `settings.xml` mirror whose `mirrorOf` is `*`, and configure the manager so your internal groupId (`com.acme`) is served only from the hosted repository and never proxied to Central. With a single mirror, a repository declared inside a project POM is not even contacted. Maven's resolver also supports remote repository filtering (prefix and groupId filters under the local repository's `.remoteRepositoryFilters` directory) to assert which groupIds a given remote may answer for. Gradle expresses the same intent with `exclusiveContent`, which ties a group to one repository so Gradle searches nowhere else for it. Reserving your groupId on Maven Central (via DNS TXT verification, or the `io.github.<user>` namespace) stops anyone else publishing under it.

For NuGet, `packageSourceMapping` makes the mapping explicit and strict: map `Acme.*` to your private feed and `*` to nuget.org, and every package, including transitive ones, must match a pattern or restore fails with `NU1101` instead of silently falling back. Start the config with `<clear />` so no inherited source sneaks in, and set a repository-local `globalPackagesFolder` so a package cached earlier from another source is not reused. Reserve your ID prefix on nuget.org so the public registry rejects a look-alike under `Acme.*`. The two snippets below are a working `settings.xml` mirror and a NuGet config; the Gradle `exclusiveContent` form is in the exercise.

```xml
<!-- ~/.m2/settings.xml, or the file your CI passes with -s -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
  <mirrors>
    <mirror>
      <id>acme-repo-manager</id>
      <name>All repository traffic goes through the repository manager</name>
      <url>https://maven.acme.example/repository/all/</url>
      <mirrorOf>*</mirrorOf>
    </mirror>
  </mirrors>
</settings>
```

## NuGet: packageSourceMapping with a repo-local cache

This `nuget.config` restricts `Acme.*` to the private feed and sends everything else to nuget.org. Because mapping is enforced during restore, an attacker's `Acme.Billing` 99.0.0 on nuget.org is never considered: the pattern says that name comes from the `acme` source only. If the private feed does not have it, restore fails rather than reaching for the public copy, which is the behaviour you want. Package source mapping does not yet cover every metadata command (for example `dotnet list package --outdated` still queries all sources), so treat it as a resolution control, not a secrecy guarantee for the name.

Across every ecosystem the artifact-repository layer (Artifactory priority resolution, Nexus routing rules, or the exclude-pattern approach) is where you enforce this centrally: a routing or priority rule says an internal namespace is served only from the hosted repository and is never requested from an upstream proxy, so the internal name never leaks to the public index and a public look-alike is never fetched.

```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <config>
    <!-- Repo-local cache: a package cached earlier from another source is
         not silently reused when a mapping would send it elsewhere. -->
    <add key="globalPackagesFolder" value=".nuget/packages" />
  </config>
  <packageSources>
    <clear />
    <add key="acme" value="https://nuget.acme.example/v3/index.json" />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <packageSourceMapping>
    <packageSource key="acme">
      <package pattern="Acme.*" />
    </packageSource>
    <packageSource key="nuget.org">
      <package pattern="*" />
    </packageSource>
  </packageSourceMapping>
</configuration>
```

## Verify names, especially AI-suggested ones

The controls above assume the name you asked for is the name you meant. Typosquatting and slopsquatting break that assumption. For human error, lean on lockfiles and the cooldown, let your SCA tool flag low-reputation packages, and let OpenSSF Scorecard's Pinned-Dependencies check flag unpinned build dependencies. For AI-suggested code, add one habit: before you add a package a model recommended, confirm it actually exists, is the one you intended, and has a plausible history (age, downloads, repository, maintainers); /domains/ai-emerging/agent-security/ai-assisted-development/ includes a check that blocks names that do not exist or are too new. OWASP's LLM Top 10 for 2026 treats registering hallucinated names as a supply-chain vector under LLM04. A cheap gate is to run installs with a cooldown and a curated allowlist so a brand-new, never-before-seen name cannot be pulled on the first build that mentions it.

None of these controls is sufficient alone. Scope or source mapping decides where a name may come from; a lockfile with hashes fixes the bytes; disabled install scripts remove the install-time execution path; a cooldown buys detection time; name verification covers the human and the model. Deploy them together and dependency confusion stops being a single missing flag away. In the OpenSSF S2C2F, binding names to sources is requirement ENF-1 at Level 2 and enforcing a curated feed is ENF-2 at Level 3; /domains/supply-chain/dependencies/s2c2f-adoption/ places them in a staged plan.

## Guided practice

Your organization publishes internal packages under @acme (npm) and the com.acme group (Maven). A reviewer finds an npm install that reaches the private registry through a repository-manager group that also proxies npmjs.com, a pip install that uses `--extra-index-url` for internal names, and a Gradle build that lists an internal repository after `mavenCentral()`. Close each gap and prove the fix with a local test, without touching production registries.

### Supplied material

```text
npm .npmrc (current):
  registry=https://npm.acme.example/group/   # proxies npmjs.com and the internal feed
  (no @acme mapping)
Internal: @acme/ledger 1.4.2 ; Public decoy on npmjs.com: @acme/ledger 1.99.0 (hypothetical)

pip (current):
  pip install --index-url https://pypi.acme.example/simple \
      --extra-index-url https://pypi.org/simple internal-reports
Internal: internal-reports 2.1.0 ; Public decoy on PyPI: internal-reports 9.9.0

Gradle (current):
  repositories { mavenCentral(); maven { url = uri("https://maven.acme.example/repository/all/") } }
Internal: com.acme:billing:1.4.2 ; Public decoy on Central: com.acme:billing:9.9.0 (hypothetical)
```

### 1. Name the resolution rule being exploited

For each of the three builds, state which source the resolver would pick for the internal name today and why.

Worked answer: npm: through a merged group with no scope map, the higher public `@acme/ledger` 1.99.0 can win. pip: `--extra-index-url` has no priority, so `internal-reports` 9.9.0 from PyPI wins. Gradle: without content filtering, `com.acme:billing` is searched on Central too and 9.9.0 wins. All three are CWE-427.

### 2. Bind each internal name to its source

Rewrite the three configurations so each internal name can resolve only from the internal source.

Worked answer: npm: add `@acme:registry` pointing at the internal feed (a scope you own cannot exist publicly). pip: drop `--extra-index-url` and resolve through one private index that proxies PyPI. Gradle: wrap the internal repo in `exclusiveContent` with `includeGroupByRegex("com\\.acme(\\..*)?")`.

### 3. Fix the bytes, not just the version

State the install command CI should use so a swapped source is caught even if a name resolves.

Worked answer: `npm ci` (fails on integrity mismatch and on lockfile/manifest drift); `pip install --require-hashes` with every requirement pinned by `==` and hashed. Optionally a lockfile host-allowlist check that fails on any unexpected resolved URL.

### 4. Prove it locally

Describe a disposable test that demonstrates each fix without using production registries.

Worked answer: Start two loopback indexes/feeds, put the internal name on one and a higher decoy on the other, and show the configured install takes the internal version. Then tamper with one cached tarball and show `npm ci` fails with an integrity error. Record versions installed and the resolved sources.

### 5. Add time and a human gate

Specify the release-age and install-script settings you would add, and what you would exclude.

Worked answer: A 3-day cooldown (npm `min-release-age` / pnpm `minimumReleaseAge` / Renovate or Dependabot) excluding `@acme/*`; keep install scripts blocked by default (npm `allowScripts` / pnpm `allowBuilds`) and approve only reviewed, pinned entries. State that these buy time and remove install-time execution but do not replace hashes or the source map.

Deliverable: Three corrected configurations, the CI install command, a local test transcript showing the internal version chosen and an integrity failure on tampering, and the cooldown/script settings with exclusions.

Scope: Decoy versions are hypothetical and the test uses loopback registries; it demonstrates resolver behaviour and integrity checking, not that any real public name is malicious. Package source mapping and content filtering control resolution, not metadata queries, so they are not a guarantee that an internal name is secret.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [MITRE CWE-427: Uncontrolled Search Path Element (dependency confusion alternate term)](https://cwe.mitre.org/data/definitions/427.html). CWE 4.20.
- [OWASP Top 10:2025 A03 Software Supply Chain Failures](https://top10.owasp.org/2025/A03_2025-Software_Supply_Chain_Failures/)
- [OWASP ASVS 5.0 V15 Secure Coding and Architecture (15.2.4 dependency confusion)](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x24-V15-Secure-Coding-and-Architecture.md). Version 5.0.0.
- [npm docs: npm install-scripts and the allowScripts policy (npm 12)](https://docs.npmjs.com/cli/v12/commands/npm-install-scripts/). npm v12.
- [npm CLI CHANGELOG: v12.0.0 breaking changes (install scripts, allow-git/allow-remote)](https://github.com/npm/cli/blob/latest/CHANGELOG.md). 12.0.0 entry (8 July 2026).
- [pip docs: pip install (index options) and secure installs (hash-checking mode)](https://pip.pypa.io/en/stable/topics/secure-installs/). pip 26.2.1.
- [uv docs: package indexes and index strategy](https://docs.astral.sh/uv/concepts/indexes/)
- [pnpm docs: dependency-resolution (minimumReleaseAge, trustPolicy) and build settings](https://pnpm.io/settings/dependency-resolution)
- [NuGet docs: package source mapping](https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping)
- [Spracklen et al., package hallucination study (arXiv 2406.10279, USENIX Security 2025)](https://arxiv.org/abs/2406.10279). v3 (March 2025).
- [OWASP Top 10 CI/CD Security Risks: CICD-SEC-3 Dependency Chain Abuse](https://owasp.org/www-project-top-10-ci-cd-security-risks/CICD-SEC-03-Dependency-Chain-Abuse). v1.0 (2022).
- [CISA, NSA and ODNI (Enduring Security Framework): Securing the Software Supply Chain, Recommended Practices Guide for Developers, section 2.3 Verify Third-Party Components](https://www.cisa.gov/sites/default/files/publications/ESF_SECURING_THE_SOFTWARE_SUPPLY_CHAIN_DEVELOPERS.PDF). August 2022.