Skip to content

Research Update Enhanced src/pentesting-web/dependency-confu... #1305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 257 additions & 17 deletions src/pentesting-web/dependency-confusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,284 @@

## Basic Information

In summary, a dependency confusion vulnerability occurs when a project is using a library with a **misspelled** name, **inexistent** or with an **unspecified version** and the used dependency repository allows to **gather updated versions from public** repositories.
Dependency Confusion (a.k.a. substitution attacks) happens when a package manager resolves a dependency name from an unintended, less-trusted registry/source (usually a public registry) instead of the intended private/internal one. This typically leads to the installation of an attacker-controlled package.

Common root causes:
- Typosquatting/misspelling: Importing `reqests` instead of `requests` (resolves from public registry).
- Non-existent/abandoned internal package: Importing `company-logging` that no longer exists internally, so the resolver looks in public registries and finds an attacker’s package.
- Version preference across multiple registries: Importing an internal `company-requests` while the resolver is allowed to also query public registries and prefers the β€œbest”/newer version published publicly by an attacker.

Key idea: If the resolver can see multiple registries for the same package name and is allowed to pick the β€œbest” candidate globally, you’re vulnerable unless you constrain resolution.

- **Misspelled**: Import **`reqests`** instead of `requests`
- **Inexistent**: Import `company-logging`, an internal library which **no longer exists**
- **Unspecified version**: Import an **internal** **existent** `company-requests` library , but the repo check **public repos** to see if there are **greater versions**.

## Exploitation

> [!WARNING]
> In all cases the attacker just need to publish a **malicious package with name** of libraries used by the victim company.
> In all cases, the attacker only needs to publish a malicious package with the same name as the dependency your build resolves from a public registry. Installation-time hooks (e.g., npm scripts) or import-time code paths often give code execution.

### Misspelled & Inexistent

If your company is trying to **import a library that isn't internal**, highly probably the repo of libraries is going to be searching for it in **public repositories**. If an attacker has created it, your code and machines running is highly probably going to be compromised.
If your project references a library that isn’t available in the private registry, and your tooling falls back to a public registry, an attacker can seed a malicious package with that name in the public registry. Your runners/CI/dev machines will fetch and execute it.

### Unspecified Version
### Unspecified Version / β€œBest-version” selection across indexes

Developers frequently leave versions unpinned or allow wide ranges. When a resolver is configured with both internal and public indexes, it may select the newest version regardless of source. For internal names like `requests-company`, if the internal index has `1.0.1` but an attacker publishes `1.0.2` to the public registry and your resolver considers both, the public package may win.

It's very common for developers to **not specify any version** of the library used, or specify just a **major version**. Then, the interpreter will try to download the **latest version** fitting those requirements.\
If the library is a **known external library** (like python `requests`), an **attacker cannot do much**, as he won't be able to create a library called `requests` (unless he is the original author).\
However, if the library is **internal**, like `requests-company` in this example, if the **library repo** allows to **check for new versions also externally**, it will search for a newer version publicly available.\
So if an **attacker knows** that the company is using the `requests-company` library **version 1.0.1** (allow minor updates). He can **publish** the library `requests-company` **version 1.0.2** and the company will **use that library instead** of the internal one.

## AWS Fix

This vulnerability was found in AWS **CodeArtifact** (read the [**details in this blog post**](https://zego.engineering/dependency-confusion-in-aws-codeartifact-86b9ff68963d)).\
AWS fixed this by allowing to specify if a library is internal or external, to avoid downloading internal dependencied from external repositories.
This vulnerability was found in AWS CodeArtifact (read the details in this blog post). AWS added controls to mark dependencies/feeds as internal vs external so the client won’t fetch β€œinternal” names from upstream public registries.


## Finding Vulnerable Libraries

In the [**original post about dependency confusion**](https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610) the author searched for thousands of exposed package.json files containing javascript project’s dependencies.
In the original post about dependency confusion the author looked for thousands of exposed manifests (e.g., `package.json`, `requirements.txt`, lockfiles) to infer internal package names and then published higher-versioned packages to public registries.


## Practical Attacker Playbook (for red teams in authorized tests)

- Enumerate names:
- Grep repos and CI configs for manifest/lock files and internal namespaces.
- Look for organization-specific prefixes (e.g., `@company/*`, `company-*`, internal groupIds, NuGet ID patterns, private module paths for Go, etc.).
- Check public registries for availability:
- If the name is unregistered publicly, register it; if it exists, attempt subdependency hijacking by targeting internal transitive names.
- Publish with precedence:
- Choose a semver that β€œwins” (e.g., a very high version) or matches resolver rules.
- Include minimal install-time execution where applicable (e.g., npm `preinstall`/`install`/`postinstall` scripts). For Python, prefer import-time execution paths, as wheels typically don’t execute arbitrary code on install.
- Exfil control:
- Ensure outbound is allowed from CI to your controlled endpoint; otherwise use DNS queries or error messages as a side-channel to prove code execution.

> [!CAUTION]
> Always get written authorization, use unique package names/versions for the engagement, and immediately unpublish or coordinate cleanup when testing concludes.


## Defender Playbook (what actually prevents confusion)

High-level strategies that work across ecosystems:
- Use unique internal namespaces and bind them to a single registry.
- Avoid mixing trust levels at resolution time. Prefer a single internal registry that proxies approved public packages instead of giving package managers both internal and public endpoints.
- For managers that support it, map packages to specific sources (no global β€œbest-version” across registries).
- Pin and lock:
- Use lockfiles that record the resolved registry URLs (npm/yarn/pnpm) or use hash/attestation pinning (pip `--require-hashes`, Gradle dependency verification).
- Block public fallback for internal names at the registry/network layer.
- Reserve your internal names in public registries when feasible to prevent future squat.


## Ecosystem Notes and Secure Config Snippets

Below are pragmatic, minimal configs to reduce or eliminate dependency confusion. Prefer enforcing these in CI and developer environments.

### JavaScript/TypeScript (npm, Yarn, pnpm)

- Use scoped packages for all internal code and pin the scope to your private registry.
- Keep installs immutable in CI (npm lockfile, `yarn install --immutable`).

.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 `@company` scope.
- For third-party packages, allow public registry via your private proxy/mirror, not directly from clients.
- Consider enabling npm package provenance for public packages you publish to increase traceability (doesn’t by itself prevent confusion).

### 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)

Use Package Source Mapping to tie package ID patterns to explicit sources and prevent resolution from unexpected feeds.

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)

Maven settings.xml (mirror all to internal; disallow ad-hoc repos in POMs via Enforcer):
```
<settings>
<mirrors>
<mirror>
<id>internal-mirror</id>
<mirrorOf>*</mirrorOf>
<url>https://maven.corp.example/repository/group</url>
</mirror>
</mirrors>
</settings>
```

Add Enforcer to ban repositories declared in POMs and force usage of your mirror:
```
<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.
- Enforce repositories in `settings.gradle(.kts)` only:
```
dependencyResolutionManagement {
repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS
repositories {
maven { url = uri("https://maven.corp.example/repository/group") }
}
}
```
- Enable dependency verification (checksums/signatures) and commit `gradle/verification-metadata.xml`.

### Go Modules

Configure private modules so the public proxy and checksum DB aren’t used for them.

```
# 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)

Replace crates.io with an approved internal mirror or vendor directory for builds; do not allow arbitrary public fallback.

.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)

Use source blocks and disable multisource Gemfiles so gems come only from the intended repository.

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 and Registry Controls That Help

- Private registry as a single ingress:
- Use Artifactory/Nexus/CodeArtifact/GitHub Packages/Azure Artifacts as the only endpoint developers/CI can reach.
- Implement block/allow rules so internal namespaces never resolve from upstream public sources.
- Lockfiles are immutable in CI:
- npm: commit `package-lock.json`, use `npm ci`.
- Yarn: commit `yarn.lock`, use `yarn install --immutable`.
- Python: commit hashed `requirements.txt`, enforce `--require-hashes`.
- Gradle: commit `verification-metadata.xml` and fail on unknown artifacts.
- Outbound 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 / attestations: when publishing public packages, enable provenance/attestations to make tampering more detectable downstream.


## References

- [https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610](https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610)
- [https://zego.engineering/dependency-confusion-in-aws-codeartifact-86b9ff68963d](https://zego.engineering/dependency-confusion-in-aws-codeartifact-86b9ff68963d)
- [https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping](https://learn.microsoft.com/en-us/nuget/consume-packages/package-source-mapping)
- [https://yarnpkg.com/configuration/yarnrc/](https://yarnpkg.com/configuration/yarnrc/)


{{#include ../banners/hacktricks-training.md}}