Software Supply Chain Security: Dependencies as Attack Vectors
A typical production application does not run code written exclusively by the team that built it. It runs code from dozens of open-source packages, each of which depends on more packages, which depend on more still. Add build tools, test frameworks, CI/CD runners, container base images, and infrastructure-as-code modules, and the set of third-party code executing in a production environment is often an order of magnitude larger than the first-party codebase.
Every component in that dependency chain is a potential attack vector. Software supply chain attacks exploit the implicit trust that application builders place in the packages, registries, and tooling they depend on. The attacker does not need to breach the target organization directly — they need to compromise something the organization already trusts.
The Structure of the Attack Surface
Understanding software supply chain risks requires mapping the chain itself. For a typical web application, that chain includes:
Direct dependencies. The packages explicitly listed in the project's manifest file. These are the ones developers think about and intentionally add. A project might have twenty or thirty direct dependencies.
Transitive dependencies. The dependencies of the direct dependencies, and their dependencies, recursively. For most applications, the transitive tree is three to ten times larger than the direct dependency list. Many of these packages are small, single-purpose utilities that were not evaluated for trustworthiness when they were pulled in — they arrived as a consequence of adding something else.
Build and development tools. Package managers, compilers, linters, test frameworks, code generators, and bundlers. These tools run locally and in CI/CD pipelines with access to source code and frequently to credentials and secrets. A compromised build tool is as dangerous as a compromised runtime library.
CI/CD infrastructure. The systems that fetch dependencies, run builds, execute tests, sign artifacts, and deploy to production. These systems often have the broadest access of any component in the development workflow — credentials, cloud access, artifact storage, deployment keys.
Container base images. The operating system layers and runtime environments that application containers are built on top of. A malicious or outdated base image introduces vulnerabilities before a single line of application code runs.
Each category has distinct attack patterns.
Dependency Confusion
Dependency confusion is an attack that exploits the behavior of package managers when they are configured to resolve packages from multiple registries — typically a public registry and a private internal one.
Organizations frequently use private package registries to host internal libraries that are not published publicly. These internal packages are referenced by name in dependency manifests, just like public packages. If the package manager checks the public registry first, or checks both and selects the higher version number, an attacker can exploit this by publishing to the public registry under the same package name.
The attack sequence:
- The attacker discovers internal package names by finding them in publicly accessible dependency manifests —
package.jsonfiles,requirements.txt,Gemfile— committed to public repositories, exposed in job listings, or visible in error messages and documentation. - The attacker publishes a package to the public registry with the discovered internal name, setting the version number higher than any plausible internal version.
- When a developer or CI/CD system runs a dependency install, the package manager resolves the public version as the highest available and fetches it.
- The malicious package executes as part of the normal build or install process.
The attack was definitively demonstrated by a researcher who published packages using names harvested from internal manifest files of dozens of large technology organizations. In each case, the package was benign — it only reported back that the resolution had succeeded — but the point was made: every organization whose internal package names reached the public internet was potentially vulnerable.
Mitigation requires explicit scoping: configuring the package manager to only resolve internal package names from the internal registry, never from public sources. For npm, this means scoping all internal packages under a namespace and configuring the registry mapping accordingly. For Python, it means using --index-url rather than --extra-index-url for internal packages. The subtlety matters: the configuration setting that adds a second registry as a fallback is different from the one that makes it authoritative.
Typosquatting on Package Registries
Package registries are large enough that most plausible names are unregistered. An attacker who registers a package with a name one character different from a popular library occupies a real namespace on the registry with effectively zero barrier.
Common typosquatting patterns include:
- Transposition.
reqeustsinstead ofrequests,lodashwith one character swapped. - Hyphen or underscore substitution.
python-dateutilversuspython_dateutil,cross-envversuscrossenv. - Common misspellings. Packages with names that match frequent autocomplete errors or regional spelling variations.
- Homoglyph substitution. Characters that are visually identical or nearly so in most fonts, such as
land1,Oand0. - Extra or missing words.
colorversuscolors,lodashversuslodash-utils.
Malicious typosquatting packages are typically designed to function identically to the package they impersonate — exporting the same interface, producing the same results — while also executing a payload. The payload might run at install time (using package lifecycle scripts like postinstall) rather than at import time, meaning the malicious code runs during npm install even if the package is never actually imported in the application code.
Several malicious packages have remained on major registries for months or years before removal, and the payload variants are increasingly evasive — running only in CI/CD environments (by detecting environment variables), only on specific operating systems, or after a time delay.
Compromised Maintainer Accounts
An attacker who gains control of a legitimate maintainer's account can publish a new version of a trusted, established package. This version reaches every project that accepts automatic updates and every build system that pulls the latest version on each run.
Maintainer account compromise vectors include:
Credential stuffing. Maintainers who reuse passwords from breached services are vulnerable to automated credential stuffing. If the registry account uses the same password as an account exposed in a data breach, that password is likely in circulation.
Phishing. Targeted phishing of maintainers of high-value packages is increasingly documented. The attack does not require breaching registry infrastructure — it only requires the maintainer to authenticate to a fake registry login page.
Account takeover through email compromise. Password reset flows for registry accounts typically rely on email. Compromising the maintainer's email account enables password resets on their registry account.
Purchasing abandoned accounts. Packages with large download counts sometimes become unmaintained when their original authors move on. Attackers have acquired ownership of such packages through legitimate mechanisms — offering to take over maintenance — and then published malicious versions.
The defense at the individual level is strong authentication (hardware security keys rather than SMS or authenticator app codes) on both the registry account and the associated email account, combined with publishing using automated signing that makes any unsigned version detectable. At the organizational level, the defense is pinning dependencies to specific versions with hash verification rather than accepting automatic updates.
Build and CI/CD Pipeline Compromise
The build pipeline is a privileged environment. It has access to source code, build secrets, signing certificates, artifact storage, deployment credentials, and often production infrastructure. Compromise of the pipeline means compromise of every artifact it produces.
Attack paths into CI/CD pipelines include:
Malicious dependencies with install-time scripts. Package lifecycle hooks (postinstall, prepare) run during dependency installation in the build environment. A malicious package with an install-time script can exfiltrate environment variables — including cloud credentials, API keys, and signing material — directly from the CI/CD runner.
Poisoned pipeline execution (PPE). If a CI/CD system builds pull requests from external contributors without restricting which pipeline definition file is used, a malicious pull request can modify the pipeline definition to inject malicious steps. The build system executes the modified pipeline, which runs with full CI access.
Compromised build action or plugin. CI/CD systems typically use a marketplace of reusable actions or plugins. A malicious or compromised action in the pipeline configuration runs with whatever access the pipeline grants. Actions with broad access to secrets are high-value targets for attackers who can compromise the action's source repository.
Build artifact tampering. An attacker with write access to artifact storage can replace built artifacts with modified versions after the legitimate build completes but before deployment. If the deployment process does not verify artifact integrity, the modified artifact reaches production.
Assessing build pipeline security requires reviewing what secrets are accessible in each pipeline stage, what external actions or plugins are used and whether they are pinned to specific versions, whether the pipeline configuration itself is protected from modification by untrusted contributors, and whether artifacts are signed and their signatures verified at deployment time.
Transitive Dependency Risk
The transitive dependency tree is where most supply chain risk lives. A project that directly depends on twenty packages may have two hundred transitive dependencies. The developers who chose those twenty packages evaluated them to varying degrees. Nobody evaluated the hundred and eighty they didn't choose — they arrived automatically.
The average npm package has more than fifty transitive dependencies. Many of those dependencies are small packages maintained by individuals with no organizational security program. A package that formats dates, pluralizes words, or pads strings may have fewer than ten lines of code and one maintainer who last published to it three years ago.
Reducing transitive dependency risk involves several approaches:
Software composition analysis. Automated tools that enumerate the full dependency tree and flag packages against known vulnerability databases, license conflicts, and package health signals (unmaintained, recently transferred ownership, new maintainers).
Dependency tree auditing. Understanding which transitive dependency provides which functionality and whether that functionality is actually used. Unused dependencies pulled in by a larger package can sometimes be excluded.
Minimal dependencies. The least-risk dependency is the one you don't have. For small, well-understood functionality (string padding, date formatting), implementing the functionality inline eliminates the dependency entirely.
Lockfile enforcement. Generating a lockfile that records specific versions and hashes for all resolved packages, and enforcing that only lockfile-recorded versions are installed in CI/CD and production build environments, prevents version substitution during builds.
Assessing Supply Chain Security
A supply chain security assessment covers the full chain from development environment to deployed artifact:
Dependency manifest review. Enumerate all direct and transitive dependencies. Flag packages that are unmaintained, recently transferred, have unusually broad install-time scripts, or have names that match typosquatting patterns against popular packages.
Registry configuration. Review how the package manager is configured when multiple registries are in use. Confirm that internal package names are explicitly scoped to internal registries and cannot be resolved from public sources.
Build pipeline review. Review CI/CD configuration for untrusted input to pipeline definitions, access to secrets in pull request builds, use of unpinned external actions, and artifact storage access controls.
Secrets in build environments. Confirm that secrets available to CI/CD runners are scoped to the minimum required and rotated if they have been accessible to any build step that fetched external dependencies.
Artifact integrity. Confirm that build artifacts are signed and that signatures are verified before deployment. Identify any gaps in the chain of custody between build completion and production deployment.
Dependency monitoring. Review whether the organization has an ongoing process for detecting newly published advisories, ownership changes, or anomalous releases for packages in the dependency tree.
The full picture of supply chain risk requires treating dependencies as untrusted third-party code — because that is what they are — rather than as inert libraries with a known-good status that persists indefinitely.
For the specific vulnerability patterns that affect web application interfaces to third-party services, see the API rate limiting bypass article and the OAuth vulnerabilities article.
Concerned about supply chain exposure in your build pipeline or dependency tree? Get in touch.