Dependency Confusion Audit & Remediation
This skill helps you identify and fix dependency confusion vulnerabilities in your project's package management setup.
What is Dependency Confusion?
Dependency confusion occurs when a package manager resolves a dependency from an unintended, less-trusted registry (usually public) instead of the intended private/internal one. Attackers exploit this by publishing malicious packages with the same names as your internal dependencies to public registries.
Common root causes:
- Typosquatting: Importing
reqestsinstead ofrequests(resolves from public registry) - Missing internal packages: Importing
company-loggingthat doesn't exist internally, so the resolver falls back to public registries - Version preference: Internal
company-requests@1.0.1vs publiccompany-requests@1.0.2— resolver picks the newer public version
Quick Audit Checklist
Run through these checks to identify vulnerabilities:
- Registry Configuration: Are you mixing internal and public registries in the same resolution?
- Internal Namespaces: Do you have organization-specific package names (e.g.,
@company/*,company-*,Company.*)? - Fallback Behavior: Does your tooling fall back to public registries when internal packages aren't found?
- Version Pinning: Are versions pinned or locked, or do you allow wide ranges?
- Lockfile Integrity: Are lockfiles committed and enforced in CI?
Ecosystem-Specific Fixes
JavaScript/TypeScript (npm, Yarn, pnpm)
Problem: npm/yarn can resolve from multiple registries and pick the "best" version.
Fix: Bind internal scopes to private registry only.
.npmrc (project-level):
# Bind internal scope to private registry; do not allow public fallback for @company/*
@company:registry=https://registry.corp.example/npm/
# Always authenticate to the private registry
//registry.corp.example/npm/:_authToken=${NPM_TOKEN}
strict-ssl=true
package.json (for internal package):
{
"name": "@company/api-client",
"version": "1.2.3",
"private": false,
"publishConfig": {
"registry": "https://registry.corp.example/npm/",
"access": "restricted"
}
}
Yarn Berry (.yarnrc.yml):
npmScopes:
company:
npmRegistryServer: "https://registry.corp.example/npm/"
npmAlwaysAuth: true
# CI should fail if lockfile would change
enableImmutableInstalls: true
Operational tips:
- Only publish internal packages within the
@companyscope - For third-party packages, allow public registry via your private proxy/mirror, not directly from clients
- Use
npm cioryarn install --immutablein CI
Python (pip / Poetry)
Core rule: Don't use --extra-index-url to mix trust levels. Either expose a single internal index that proxies and caches approved PyPI packages, or use explicit index selection and hash pinning.
pip.conf:
[global]
index-url = https://pypi.corp.example/simple
# Disallow source distributions when possible
only-binary = :all:
# Lock with hashes generated via pip-tools
require-hashes = true
Generate hashed requirements with pip-tools:
# From pyproject.toml or requirements.in
pip-compile --generate-hashes -o requirements.txt
pip install --require-hashes -r requirements.txt
If you must reach public PyPI, do it via your internal proxy and maintain an explicit allowlist there. Avoid --extra-index-url in CI.
.NET (NuGet)
Problem: Multiple package sources can be queried simultaneously.
Fix: Use Package Source Mapping to bind patterns to specific sources.
nuget.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="corp" value="https://nuget.corp.example/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="corp">
<package pattern="Company.*" />
<package pattern="Internal.Utilities" />
</packageSource>
</packageSourceMapping>
</configuration>
Java (Maven/Gradle)
Problem: POMs can declare arbitrary repositories.
Fix: Mirror all to internal, ban ad-hoc repos.
Maven settings.xml:
<settings>
<mirrors>
<mirror>
<id>internal-mirror</id>
<mirrorOf>*</mirrorOf>
<url>https://maven.corp.example/repository/group</url>
</mirror>
</mirrors>
</settings>
Add Enforcer plugin to ban repositories in POMs:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>enforce-no-repositories</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<requireNoRepositories />
</rules>
</configuration>
</execution>
</executions>
</plugin>
Gradle: Centralize and lock dependencies.
settings.gradle.kts:
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven { url = uri("https://maven.corp.example/repository/group") }
}
}
Enable dependency verification and commit gradle/verification-metadata.xml.
Go Modules
Problem: Public proxy can resolve private module names.
Fix: Mark private paths to skip proxy.
# Use corporate proxy first, then public proxy as fallback
export GOPROXY=https://goproxy.corp.example,https://proxy.golang.org
# Mark private paths to skip proxy and checksum db
export GOPRIVATE=*.corp.example.com,github.com/your-org/*
export GONOSUMDB=*.corp.example.com,github.com/your-org/*
Rust (Cargo)
Problem: crates.io fallback for internal packages.
Fix: Replace crates-io with internal mirror.
.cargo/config.toml:
[source.crates-io]
replace-with = "corp-mirror"
[source.corp-mirror]
registry = "https://crates-mirror.corp.example/index"
For publishing, be explicit with --registry and keep credentials scoped to the target registry.
Ruby (Bundler)
Problem: Multi-source Gemfiles can mix registries.
Fix: Use source blocks and disable multisource.
Gemfile:
source "https://gems.corp.example"
source "https://rubygems.org" do
gem "rails"
gem "pg"
end
source "https://gems.corp.example" do
gem "company-logging"
end
Enforce at config level:
bundle config set disable_multisource true
CI/CD Hardening
- Single ingress: Use Artifactory/Nexus/CodeArtifact/GitHub Packages/Azure Artifacts as the only endpoint developers/CI can reach
- Immutable lockfiles: Fail CI if lockfile would change
- npm: commit
package-lock.json, usenpm ci - Yarn: commit
yarn.lock, useyarn install --immutable - Python: commit hashed
requirements.txt, enforce--require-hashes - Gradle: commit
verification-metadata.xmland fail on unknown artifacts
- npm: commit
- Egress control: Block direct access from CI to public registries except via the approved proxy
- Name reservation: Pre-register your internal names/namespaces in public registries where supported
- Package provenance: When publishing public packages, enable provenance/attestations to make tampering more detectable
When to Use This Skill
Use this skill when:
- Reviewing package configuration files (
.npmrc,pip.conf,nuget.config,settings.xml,Gemfile, etc.) - Setting up a new project's dependency management
- Auditing CI/CD pipelines for supply chain security
- Investigating a potential dependency confusion incident
- Wanting to harden your build environment against supply chain attacks
- Configuring private registries or internal package management