Dependency Confusion and Artifact Poisoning: Attack Techniques and Defenses

Introduction

Software supply chain attacks have surged in both frequency and sophistication over the past several years. Rather than attacking applications directly, adversaries increasingly target the dependency resolution and artifact distribution layers that underpin modern software development. Two of the most effective techniques in this category are dependency confusion and artifact poisoning.

These attacks exploit a fundamental truth: modern software is assembled, not written from scratch. A typical application may pull in hundreds or thousands of third-party packages, container images, CI templates, and build plugins — each representing a link in a chain of trust. When any link is compromised, the entire chain fails.

What makes these attacks particularly dangerous is that many organizations remain vulnerable despite having mature security programs. Traditional application security testing does not catch a malicious dependency installed at build time. Firewalls and WAFs are irrelevant when the attacker’s code runs inside your CI/CD pipeline with full network access and production credentials.

This guide provides an in-depth examination of how dependency confusion and artifact poisoning work, why CI/CD pipelines are prime targets, and — most importantly — what practical defenses you can implement today.

Dependency Confusion Explained

How Package Managers Resolve Names

Most modern package managers — npm, pip, RubyGems, NuGet, Maven — follow a resolution process that checks one or more registries when a package is requested. When an organization uses both a private/internal registry (for proprietary packages) and the public registry (for open-source packages), the package manager must decide which registry takes priority when both contain a package with the same name.

The default behavior varies by ecosystem, but a common pattern is troubling: many package managers will prefer the higher version number regardless of which registry it comes from. This seemingly innocent design choice is the foundation of the dependency confusion attack.

The Original Research: Alex Birsan (2021)

In February 2021, security researcher Alex Birsan published groundbreaking research demonstrating how this resolution behavior could be weaponized. By examining publicly leaked internal package names from companies like Apple, Microsoft, and Tesla — found in JavaScript files, package manifests, and error messages — he registered identically named packages on the public npm, PyPI, and RubyGems registries with inflated version numbers.

The result was devastating. When these companies’ build systems resolved dependencies, the package managers fetched Birsan’s public packages instead of the legitimate internal ones. His proof-of-concept packages included benign callbacks that phoned home, confirming code execution inside corporate networks. Birsan earned over $130,000 in bug bounties across multiple organizations.

The Attack Mechanics

The dependency confusion attack follows a straightforward sequence:

  1. Reconnaissance: The attacker identifies internal/private package names used by the target organization. These can be found in leaked package.json files, JavaScript source maps, error messages, job postings, or open-source repositories that reference internal dependencies.
  2. Registration: The attacker registers a package with the identical name on the corresponding public registry (npm, PyPI, etc.), assigning it a very high version number (e.g., 99.0.0).
  3. Payload: The public package contains malicious code in install scripts or post-install hooks — code that executes automatically when the package is installed.
  4. Execution: When the target organization’s build system runs npm install, pip install, or an equivalent command, the package manager resolves the dependency to the attacker’s public package due to the higher version number.
  5. Compromise: The malicious install script runs with the privileges of the build process, typically gaining access to environment variables (including secrets), network resources, and source code.

Affected Ecosystems

Dependency confusion is not limited to a single language or ecosystem. The following are all susceptible:

  • npm (Node.js): Default behavior can prefer public packages over private ones when scoping is not used.
  • PyPI (Python): pip’s --extra-index-url flag checks both the private and public indexes, preferring the higher version.
  • RubyGems (Ruby): Similar resolution behavior when multiple sources are configured.
  • NuGet (.NET): Checks multiple configured feeds and can prefer the public gallery.
  • Maven (Java): Resolves from multiple repositories; attackers can publish to Maven Central with matching group/artifact IDs.

Install Scripts as the Payload Vector

The reason dependency confusion is so dangerous is that package managers support automatic code execution during installation. In npm, this happens through preinstall, install, and postinstall scripts defined in package.json. In Python, setup.py can execute arbitrary code during pip install. These hooks were designed for legitimate build tasks but provide attackers with an ideal execution vector — code runs before the application is even built, often with elevated privileges.

Artifact Poisoning: Beyond Packages

While dependency confusion targets package registries specifically, artifact poisoning is a broader category of supply chain attacks that can target any external artifact consumed during the software development lifecycle. The attack surface extends far beyond package managers.

Compromised Container Base Images

Container images pulled from Docker Hub or other public registries are a common attack vector. An attacker can publish a malicious image with a name similar to a popular base image, or compromise an existing image by gaining access to the maintainer’s account. If your Dockerfile specifies FROM python:3.11 using a mutable tag, a compromised image pushed to that tag will be pulled into every subsequent build.

Tampered CI/CD Templates

GitHub Actions and GitLab CI templates referenced from public repositories represent another significant attack surface. When a workflow references uses: some-org/some-action@main, the code executed in your pipeline is controlled by whoever has push access to that repository. If the action’s repository is compromised, every pipeline that references it becomes compromised as well.

Typosquatting

Typosquatting attacks exploit common misspellings and visual similarities in package names. Examples include registering co1ors (with a numeral one) instead of colors, lodahs instead of lodash, or reqeusts instead of requests. These packages contain malicious code and rely on developers making typographical errors when adding dependencies. Automated tools have detected thousands of typosquatting packages across npm and PyPI.

Hijacked Maintainer Accounts

When an attacker gains access to a legitimate package maintainer’s account — through credential stuffing, phishing, or social engineering — they can publish backdoored versions of widely-used packages. Because the package name and maintainer are legitimate, these compromised versions are extremely difficult to detect through automated means.

Build Tool Plugins

Build systems like Gradle, Maven, and webpack support plugins that execute during the build process. Malicious or compromised plugins in these ecosystems can modify build outputs, exfiltrate secrets, or inject backdoors into compiled artifacts. Because build plugins are often less scrutinized than application dependencies, they represent a high-value target.

Real-World Incidents

Several major incidents illustrate the real-world impact of artifact poisoning:

  • event-stream (2018): A new maintainer was granted publishing rights to the popular npm package event-stream (1.5 million weekly downloads). They added a dependency on a malicious package, flatmap-stream, which contained encrypted code targeting the Copay Bitcoin wallet, attempting to steal cryptocurrency.
  • ua-parser-js (2021): The npm package ua-parser-js (7 million weekly downloads) was hijacked when the maintainer’s account was compromised. Malicious versions were published that installed cryptominers and credential-stealing malware on Linux and Windows systems.
  • node-ipc (2022): The maintainer of the node-ipc package deliberately added code that wiped files on systems with Russian or Belarusian IP addresses, demonstrating that even trusted maintainers can become a threat vector (sometimes called “protestware”).

How These Attacks Exploit CI/CD

CI/CD pipelines are uniquely vulnerable to dependency confusion and artifact poisoning because of how they are designed to operate. Understanding why pipelines are prime targets is essential for building effective defenses.

Automatic Code Execution During Builds

Every time a CI pipeline runs npm install, pip install -r requirements.txt, or docker build, it is executing code from external sources. This happens automatically on every commit, pull request, or scheduled build. There is no human in the loop to review what code is actually being pulled in and executed.

Build Environments Have Access to Credentials

CI/CD environments are typically configured with secrets needed for deployment: cloud provider credentials, API tokens, database passwords, signing keys, and registry credentials. A malicious dependency that runs during the build phase can access these secrets through environment variables or mounted secret stores. This makes CI/CD pipelines far more valuable targets than developer laptops.

Dependencies Fetched at Build Time Are Not Pre-Audited

In most organizations, dependency versions are specified in manifest files (package.json, requirements.txt) but the actual code is fetched fresh from registries at build time. Between when a developer adds a dependency and when the CI system installs it, the package contents could change — or a dependency confusion package could appear on the public registry. There is typically no verification step between resolution and execution.

Transitive Dependencies Expand the Attack Surface

Your application may declare 50 direct dependencies, but those dependencies have their own dependencies, creating a tree that can include thousands of transitive packages. You have no direct control over what your dependencies depend on. When a transitive dependency is compromised — as in the event-stream incident — the attack propagates through the entire dependency tree without any change to your own manifest files.

Defending Against Dependency Confusion

Preventing dependency confusion requires configuring your package managers and registries to eliminate the ambiguity between public and private packages. Here are the most effective mitigations.

Namespace and Scope Your Private Packages

The single most effective defense is to use namespaced or scoped package names for all internal packages. In npm, this means using scoped packages like @yourcompany/package-name. An attacker cannot register packages under your organization’s scope on the public npm registry.

Configure Registry Priority Explicitly

Never rely on default resolution behavior. Explicitly configure your package manager to fetch scoped packages from your private registry and everything else from the public registry.

Example .npmrc configuration:

# Always fetch @yourcompany scoped packages from private registry
@yourcompany:registry=https://npm.yourcompany.com/

# All other packages come from the public npm registry
registry=https://registry.npmjs.org/

# Authentication for private registry
//npm.yourcompany.com/:_authToken=${NPM_PRIVATE_TOKEN}

Example pip.conf for Python:

# IMPORTANT: Use --index-url (NOT --extra-index-url) for your private registry
# --extra-index-url checks BOTH registries and picks the higher version (vulnerable!)
# --index-url uses ONLY your private registry as the primary source

[global]
index-url = https://pypi.yourcompany.com/simple/

# If you need public PyPI packages, configure your private registry
# (Artifactory, Nexus) to proxy public PyPI — do NOT use --extra-index-url

Example .yarnrc.yml for Yarn Berry:

npmScopes:
  yourcompany:
    npmRegistryServer: "https://npm.yourcompany.com"
    npmAuthToken: "${NPM_PRIVATE_TOKEN}"

npmRegistryServer: "https://registry.yarnpkg.com"

Use Private Registry Proxies

Deploy a registry proxy such as JFrog Artifactory, Sonatype Nexus, or GitHub Packages that sits between your build systems and public registries. Configure the proxy to:

  • Serve internal packages from your private repository.
  • Proxy public packages from the upstream registry.
  • Block any public package that shares a name with an internal package.
  • Apply security policies (vulnerability scanning, license compliance) before allowing packages through.

This creates a single source of truth for all dependencies and eliminates the public-vs-private ambiguity entirely.

Defensive Registration

Register your internal package names on public registries as placeholder packages. These packages should contain no real code — just a README explaining that the name is reserved. This prevents attackers from claiming those names. While this is not a primary defense, it adds an extra layer of protection.

Pin Dependencies by Hash

Pinning dependencies by cryptographic hash ensures that the exact artifact you audited is the one installed during builds. Even if an attacker publishes a malicious version, the hash will not match and the installation will fail.

For pip (Python):

# Generate hashes for your requirements
pip-compile --generate-hashes requirements.in -o requirements.txt

# Install with hash verification
pip install --require-hashes -r requirements.txt

For npm:

npm automatically records integrity hashes in package-lock.json. Ensure you commit the lockfile and use npm ci (not npm install) in CI to enforce integrity verification:

# In CI, always use npm ci — it strictly follows the lockfile
# and verifies integrity hashes for every package
npm ci

Defending Against Artifact Poisoning

Because artifact poisoning encompasses a wider range of attack vectors, defense requires layered controls across container images, CI templates, dependencies, and build processes.

Pin Container Images by Digest

Never reference container images by mutable tags like latest or even 3.11. Instead, pin to the immutable SHA256 digest:

# Vulnerable: tag can be overwritten with a compromised image
FROM python:3.11-slim

# Secure: digest is immutable — this exact image or nothing
FROM python:3.11-slim@sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Pin GitHub Actions by SHA

Reference GitHub Actions by their full commit SHA rather than a mutable tag:

# Vulnerable: v3 tag can be moved to point to compromised code
- uses: actions/checkout@v3

# Secure: pinned to specific commit SHA
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2

Verify Signatures on Dependencies

Where available, verify cryptographic signatures to confirm that artifacts were published by their expected maintainers:

  • npm: Use npm audit signatures to verify registry signatures on packages.
  • Container images: Use Sigstore/cosign to verify container image signatures.
  • Python: PEP 740 and tools like sigstore-python are bringing signature verification to PyPI.

Generate and Track SBOMs

A Software Bill of Materials (SBOM) inventories every component in your application. By generating SBOMs for each build and comparing them, you can detect unexpected additions or changes to your dependency tree. Tools like Syft, Trivy, and CycloneDX can generate SBOMs in standard formats (SPDX, CycloneDX).

Automate Dependency Review

Deploy automated tools that continuously scan your dependencies for known vulnerabilities and suspicious changes:

  • Dependabot / Renovate: Automatically create PRs when dependency updates are available, giving you a chance to review before merging.
  • npm audit / pip-audit: Scan for known vulnerabilities in your dependency tree.
  • GitHub Dependency Review Action: Blocks PRs that introduce dependencies with known vulnerabilities.

Restrict Network Access During Builds (Hermetic Builds)

Hermetic builds are builds that cannot access the network. All dependencies must be pre-fetched and cached before the build begins. This prevents a build from fetching a newly published malicious package. Bazel natively supports hermetic builds, and similar isolation can be achieved with Docker’s --network=none flag or CI platform-specific network policies.

Pre-Approve and Allowlist Dependencies

Maintain an approved list of dependencies and their versions. Any new dependency or version change requires explicit approval through a review process. While this adds friction, it prevents unauthorized packages from entering your build pipeline.

Detection and Monitoring

Even with strong preventive controls, detection capabilities are essential for catching attacks that bypass your defenses.

Monitor for Unexpected New Dependencies

Implement CI checks that flag pull requests introducing new dependencies. Require explicit justification and review for any addition to your dependency manifest. This is particularly important for transitive dependencies — a new direct dependency might bring dozens of transitive ones.

Alert on Dependency Version Jumps

A dependency jumping from version 1.2.3 to 99.0.0 is a strong indicator of a dependency confusion attack. Implement monitoring that alerts on unusual version changes, especially large major version jumps in internal packages.

Leverage Security Scanning Tools

  • Socket.dev: Analyzes package behavior (network access, filesystem access, install scripts) rather than just known CVEs, making it effective at detecting supply chain attacks.
  • Snyk: Provides vulnerability scanning and monitoring across multiple ecosystems.
  • GitHub Dependency Graph and Dependabot Alerts: Automatically tracks dependencies and alerts on known vulnerabilities.

Scan for Install Scripts and Post-Install Hooks

Implement tooling that specifically identifies packages with install scripts. While install scripts have legitimate uses, they are the primary execution vector for dependency confusion attacks. Flag and review any package that includes preinstall, install, or postinstall scripts in npm, or a setup.py with executable code in Python.

Compare SBOMs Between Builds

Regularly diff SBOMs from consecutive builds. Unexpected changes — new packages appearing, versions changing without corresponding manifest updates, or packages from unexpected registries — should trigger alerts and investigation.

CI/CD-Specific Hardening

Beyond dependency management, your CI/CD pipeline configuration itself needs hardening to resist supply chain attacks.

Commit and Verify Lock Files in CI

Lock files (package-lock.json, yarn.lock, Pipfile.lock, poetry.lock) record the exact versions and hashes of every dependency. Your CI pipeline should fail if the lock file has unexpected changes.

GitHub Actions example — verify lockfile integrity:

name: Build
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2

      - name: Setup Node.js
        uses: actions/setup-node@1a4442cacd436585916f1b3aa94e4166f1a22160 # v3.8.2
        with:
          node-version: '20'

      - name: Verify lockfile has not been tampered with
        run: |
          # npm ci will fail if package-lock.json is out of sync
          # with package.json or if integrity hashes don't match
          npm ci

      - name: Check for lockfile modifications
        run: |
          if ! git diff --exit-code package-lock.json; then
            echo "ERROR: package-lock.json was modified during install."
            echo "This could indicate a dependency confusion attack."
            exit 1
          fi

      - name: Audit dependencies
        run: npm audit --audit-level=high

GitLab CI example — lockfile verification with pip:

stages:
  - verify
  - build
  - test

verify-dependencies:
  stage: verify
  image: python:3.11-slim@sha256:abc123...  # Pin by digest
  script:
    - pip install pip-tools pip-audit
    # Verify that requirements.txt hashes match actual packages
    - pip install --require-hashes --no-deps -r requirements.txt
    # Audit for known vulnerabilities
    - pip-audit -r requirements.txt
    # Ensure no unexpected changes to lockfile
    - pip-compile --generate-hashes requirements.in -o /tmp/requirements-check.txt
    - diff requirements.txt /tmp/requirements-check.txt
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

build:
  stage: build
  image: python:3.11-slim@sha256:abc123...
  script:
    - pip install --require-hashes --no-deps -r requirements.txt
    - python -m build
  needs: [verify-dependencies]

Separate Dependency Resolution from Build Execution

Use a two-phase build process: first resolve and download dependencies in an isolated environment, then run the actual build with network access disabled. This prevents a compromised dependency from exfiltrating data or downloading additional payloads during the build.

Air-Gapped or Network-Restricted Build Environments

For high-security builds, use network-restricted environments that can only access your internal registry proxy. This eliminates the possibility of dependencies being fetched directly from public registries during the build.

# Docker-based hermetic build example
# Phase 1: Fetch dependencies (with network)
docker run --name dep-fetch my-builder:latest \
  npm ci --prefer-offline

# Phase 2: Build (without network)
docker run --network=none -v deps:/app/node_modules \
  my-builder:latest npm run build

Cache Dependencies in Trusted Internal Storage

Instead of fetching dependencies from public registries on every build, cache approved versions in internal storage (Artifactory, Nexus, cloud storage buckets). Your CI pipeline should pull exclusively from this trusted cache. Update the cache through a controlled, audited process.

Conclusion

Dependency confusion and artifact poisoning exploit deeply embedded trust assumptions in the software supply chain. Every npm install, every docker pull, every uses: directive in a GitHub Actions workflow is a trust decision — and attackers are actively working to abuse that trust.

Effective defense is not a single tool or configuration change. It requires controls at every layer of the dependency lifecycle:

  • Resolution: Scope your packages, configure registry priority, use proxy registries to eliminate public/private ambiguity.
  • Installation: Pin by hash, use lockfiles with npm ci or --require-hashes, disable install scripts where possible.
  • Verification: Verify signatures, compare SBOMs, audit dependencies before they enter your build.
  • Monitoring: Detect unexpected dependency changes, alert on version anomalies, scan for malicious behavior in packages.
  • Pipeline hardening: Restrict network access during builds, verify lockfile integrity in CI, separate dependency resolution from build execution.

The organizations that will be most resilient to supply chain attacks are those that treat dependencies with the same rigor they apply to their own code: reviewed, verified, monitored, and never implicitly trusted. Start by implementing the highest-impact controls — scoped packages, registry configuration, lockfile verification in CI — and progressively add layers as your supply chain security program matures.