Skip to main content

Chapter 2: Dependency Confusion & Substitution Attacks

1. The Concept (ELI5)

Imagine you have an internal company phonebook. If you need to call "Bob from Accounting," you look him up in the company book. But what if someone prints a public phonebook, also includes a "Bob from Accounting" with a higher priority rating, and drops it on your desk? When you dial, you accidentally call an imposter.

Dependency Confusion happens when a package manager (like pip, npm, or gem) is configured to look for packages in both a private, internal registry and the public registry (like npmjs.com or PyPI). If an attacker registers a package on the public registry with the exact same name as your internal package, but assigns it a ridiculously high version number (e.g., 99.9.9), the package manager might get confused and pull the malicious public version instead of your secure private one.

2. The Visual

3. The Code: Mitigating Dependency Confusion

❌ Vulnerable Code (Misconfigured Registries)

Python (pip requirements.txt / pip.conf):

# Vulnerable: specifying the extra-index-url causes pip to search PyPI as well as the private repo.
# If an attacker claims the package on PyPI, pip might install it.
[global]
index-url = https://pypi.org/simple
extra-index-url = https://my-company.com/artifactory/api/pypi/simple

TypeScript/Node (npm .npmrc):

# Vulnerable: No scope definition. All packages fallback to public registry if version is higher.
registry=https://registry.npmjs.org/
@mycompany:registry=https://npm.pkg.github.com
# But developers might just use `internal-util` without the scope!

Go (go env):

# Vulnerable: GOPRIVATE is not set, causing Go to attempt to fetch internal repos through the public proxy.
GOPROXY="https://proxy.golang.org,direct"

✅ Production-Ready Secure Code

Python (Secure pip configuration): Instead of extra-index-url, use --index-url strictly for the private repo (which acts as a proxy/cache for public packages), or use Hash-Checking mode.

# Secure: Only point to the internal Artifactory/Nexus, which handles safe upstreaming.
[global]
index-url = https://my-company.com/artifactory/api/pypi/simple

In requirements.txt, enforce hashes:

internal-auth-lib==1.2.0 --hash=sha256:d8c1...

TypeScript/Node (Strict Scopes): Always use npm scopes (@company/package) and map ONLY that scope to the private registry. Prevent publishing internal packages externally.

# Secure .npmrc
# Map the specific scope to the private registry
@mycompany:registry=https://npm.pkg.github.com
# Enforce strict SSL
strict-ssl=true
// package.json
{
"name": "@mycompany/internal-auth-lib",
"version": "1.2.0",
"private": true // Prevents accidental public publishing
}

Go (Secure GOPRIVATE): Instruct the Go toolchain to bypass the public proxy and checksum database for specific internal paths.

# Secure: Tell Go that github.com/mycompany/* is private
go env -w GOPRIVATE="github.com/mycompany/*"

4. The Guardrail

To automatically prevent dependency confusion in CI/CD, we can use Semgrep to detect vulnerable pip.conf or requirements.txt setups, and use infrastructure-as-code policies to enforce artifact proxying.

Semgrep Rule (Detecting --extra-index-url):

rules:
- id: pip-extra-index-url-dependency-confusion
languages: [bash, generic]
paths:
include:
- "pip.conf"
- "requirements.txt"
- "Dockerfile"
message: "Using `--extra-index-url` can lead to dependency confusion attacks. Attackers can register a package with the same name on the public registry and pip will prioritize it. Use a private artifact repository proxy instead."
severity: ERROR
patterns:
- pattern-regex: "(?i).*--extra-index-url.*"

Terraform (AWS CodeArtifact config preventing public upstream routing for specific prefixes):

resource "aws_codeartifact_repository" "internal_repo" {
repository = "company-internal"
domain = aws_codeartifact_domain.main.domain

# Secure: Do not attach an external upstream like PyPI directly for internal prefixes
external_connections {
external_connection_name = "public:npmjs"
}
}

# Policy to restrict publishing/fetching
resource "aws_codeartifact_repository_permissions_policy" "strict_policy" {
repository = aws_codeartifact_repository.internal_repo.repository
domain = aws_codeartifact_domain.main.domain
policy_document = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = ["codeartifact:PublishPackageVersion"]
Effect = "Allow"
Principal = { AWS = var.ci_role_arn }
Resource = "*"
# Ensure packages must match strict internal naming conventions
Condition = {
StringLike = { "codeartifact:Package" : "@mycompany/*" }
}
}
]
})
}