← Back

CICD-SEC-3: Dependency Chain Abuse - Code Examples

Each pair below shows an insecure package-manager configuration and the secure version for the same ecosystem. The examples focus on the settings that decide the attack: which registry answers, how versions and hashes are verified, whether private names can be shadowed, and whether install-time scripts run.

npm / Node.js

Insecure

# .npmrc - public registry is the only source; scopes fall back publicly
registry=https://registry.npmjs.org/
# No @acme scope mapping, so @acme (and unscoped internal names) can be
# resolved from the PUBLIC registry -> dependency-confusion window open.

# package.json - floating ranges, unscoped internal name
{
  "dependencies": {
    "acme-auth-utils": "^1.0.0",   // internal name, unscoped, floating
    "lodash": "*"                  // any version the registry serves
  }
}
# Build step - loose install that trusts whatever resolves and runs scripts
npm install            # regenerates lock, honors highest match, runs postinstall
# A public "acme-auth-utils@99.99.99" wins and its postinstall executes.

Secure

# .npmrc - single internal proxy; @acme bound PRIVATELY with no public fallback
registry=https://registry.internal.acme/npm/
@acme:registry=https://registry.internal.acme/npm/
//registry.internal.acme/npm/:_authToken=${NPM_TOKEN}
always-auth=true
ignore-scripts=true            # no lifecycle scripts during install

# package.json - scoped internal name, exact versions
{
  "dependencies": {
    "@acme/auth-utils": "1.4.2",   // scoped -> only the private registry
    "lodash": "4.17.21"            // exact version, matched to the lockfile
  }
}
# Build step - locked, hash-verified, script-free install in CI
npm ci --ignore-scripts
#   * fails if package.json and package-lock.json disagree
#   * installs the EXACT, sha512-integrity-checked artifacts from the lock
#   * runs no postinstall/preinstall scripts
npm audit signatures           # verify registry signatures/provenance

pip / Python

Insecure

# pip.conf / CLI - private index MERGED with public PyPI
[global]
index-url = https://pypi.org/simple
extra-index-url = https://pypi.internal.acme/simple
#   pip queries BOTH and picks the highest version across them.
#   A public "acme-auth-utils" at a higher version shadows the private one.

# requirements.txt - no pins, no hashes
acme-auth-utils        # unpinned internal name
requests               # unpinned
# Install - sdists allowed, so setup.py can run arbitrary code
pip install -r requirements.txt
# No hash verification; malicious setup.py executes on the build agent.

Secure

# pip.conf - a SINGLE internal index; no extra-index-url merging
[global]
index-url = https://pypi.internal.acme/simple
require-hashes = true
only-binary = :all:            # prefer wheels; do not execute setup.py

# requirements.txt - pinned versions + integrity hashes
acme-auth-utils==1.4.2 \
  --hash=sha256:9f2b3a1c0d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
requests==2.32.3 \
  --hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760
# Install - hashes enforced; any unpinned/altered package is rejected
pip install --require-hashes --only-binary=:all: -r requirements.txt
#   --require-hashes: every requirement MUST carry a matching hash
#   --only-binary:    no source builds, so no setup.py runs at install

Maven / Java

Insecure

<!-- pom.xml / settings.xml - public Maven Central reachable directly,
     version RANGES allowed, no checksum enforcement -->
<repositories>
  <repository>
    <id>central</id>
    <url>https://repo.maven.apache.org/maven2</url>  <!-- public, direct -->
  </repository>
</repositories>

<dependency>
  <groupId>com.acme</groupId>
  <artifactId>auth-utils</artifactId>
  <version>[1.0,)</version>   <!-- open range: highest available wins -->
</dependency>

Secure

<!-- settings.xml - mirror EVERYTHING through the internal repository -->
<mirrors>
  <mirror>
    <id>internal</id>
    <mirrorOf>*</mirrorOf>   <!-- no direct Central access -->
    <url>https://nexus.internal.acme/repository/maven/</url>
  </mirror>
</mirrors>

<!-- pom.xml - exact version, group you own and have verified -->
<dependency>
  <groupId>com.acme</groupId>
  <artifactId>auth-utils</artifactId>
  <version>1.4.2</version>   <!-- exact, no range -->
</dependency>
# Build - fail hard on any checksum mismatch
mvn -C clean verify        # -C = strict checksum policy (fail, don't warn)
# Combine with the OWASP dependency-check plugin for known-bad versions.

Scoping vs. Confusion at a Glance

Unscoped, public fallback ON:
  "acme-auth-utils"  ->  resolver may accept a PUBLIC match  ->  confusion

Scoped, private binding, NO fallback:
  "@acme/auth-utils" ->  ONLY the internal registry answers  ->  safe
  (+ @acme claimed on the public registry so it cannot be squatted)

CI Pipeline: Insecure vs. Secure Install Stage

Insecure

build:
  image: node:20
  script:
    - npm install                 # loose, regenerates lock, runs scripts
    - npm run build
  # Full deploy secrets present in this environment; unrestricted egress.

Secure

install:
  image: node:20-slim
  script:
    - npm ci --ignore-scripts     # locked, hash-verified, no scripts
    - npm audit --audit-level=high
    - npm audit signatures        # provenance/signature check
  # No deploy secrets here; egress restricted; ephemeral, non-root user.

deploy:
  needs: [install]                # secrets injected only AFTER a clean install
  script: ./publish.sh            # operates on verified artifacts only

What Changed, and Why

ControlInsecureSecure
Registry sourcePublic registry direct, or public+private mergedSingle internal proxy/registry, allow-listed
Internal namesUnscoped, shadowable publiclyScoped, private-only binding, claimed publicly
VersionsRanges / * / open rangesExact pins matched to a lockfile
IntegrityNo hash verificationnpm ci / --require-hashes / strict checksums
Install scriptsRun automatically (postinstall, setup.py)Disabled / wheels-only, allow-list exceptions
ProvenanceNoneSignature/provenance verification
AgentSecrets present, broad egressLeast-privilege, isolated install stage

Next Steps