Cybersecurity for Developers
Hands-onBeginner 30 min· Secure coding · 8 of 13

Dependencies and supply-chain security

Learn how attackers use the packages you install, then practise the habits that protect you: auditing, lockfiles, hash pinning, vetting new packages and automatic updates with Dependabot.

Almost every app you build is mostly code you did not write. A small React project can pull in hundreds of packages once you count the dependencies of your dependencies. Each one is code that runs on your laptop, your build server or your users' browsers. A flaw in any of them, or a malicious package that looks harmless, becomes your problem. This is the software supply chain.

In this tutorial you will learn how supply-chain attacks happen, then practise the habits that reduce the risk: auditing, lockfiles, pinning, automated updates and a quick review before adding a new package. Prerequisites: Node.js and npm installed, or Python with pip, and an existing project folder (the Brew & Bean website or API will do). Everything here is done on your own projects.

How supply-chain problems happen

Common supply-chain risks

ProblemWhat happensExample
Known vulnerabilityA real package has a bug that attackers can exploitAn old version of a web framework with a public CVE
TyposquattingA malicious package has a name close to a popular onereqeusts instead of requests
Dependency confusionA build picks a public package instead of your private one with the same nameYour internal package acme-utils replaced by a public acme-utils
Account takeoverAn attacker steals a maintainer's login and publishes a bad versionA new patch release that quietly sends environment variables to a server
Install scriptsA package runs code the moment you install itnpm postinstall scripts
Abandoned packageNobody fixes bugs any moreA library with no release in years

A CVE (Common Vulnerabilities and Exposures) is a public ID, like CVE-2021-44228, for one known vulnerability. Tools compare the exact versions in your project with databases of CVEs and tell you which ones you use.

The routine in this tutorial

  1. 1Look at what you depend on (Step 1).
  2. 2Audit for known vulnerabilities (Step 2).
  3. 3Lock and pin exact versions (Step 3).
  4. 4Vet every new package before adding it (Step 4).
  5. 5Automate updates and checks (Step 5).

Step 1: Know what you depend on

Direct dependencies are the ones you chose. Transitive dependencies are what those packages need. Vulnerabilities often hide in the transitive ones, so look at the whole tree first.

Terminal
# Node: list the dependency tree (2 levels deep)
npm ls --depth=2

# Why is a package in my project?
npm explain lodash

# Python: list installed packages
pip list

# Python: tree view (install once)
pip install pipdeptree
pipdeptree

npm explain shows which of your dependencies pulled a package in. pipdeptree prints Python packages as a tree.

Step 2: Audit for known vulnerabilities

Terminal
# Node: check against the npm advisory database
npm audit

# Only show serious ones
npm audit --audit-level=high

# Python: install and run pip-audit on your requirements file
pip install pip-audit
pip-audit -r requirements.txt

npm audit ships with npm. pip-audit checks your Python packages against known advisories.

Example of npm audit output (yours will differ)
# npm audit report

semver  7.0.0 - 7.5.1
severity: high
semver vulnerable to Regular Expression Denial of Service
fix available via `npm audit fix`

1 high severity vulnerability

Read each finding. Ask three questions: Is the vulnerable code actually reachable in my app? Is there a fixed version? Does the fix break my app? A vulnerability in a build-only tool is less urgent than one in code that handles user input on your server.

Terminal
# Apply fixes that do not need a major version change
npm audit fix

# Then run your tests and the app before committing
npm test

npm audit fix updates to a safe version where it can. Avoid --force unless you have read what it will upgrade, because it may install breaking major versions.

A clean report is not a guarantee

An audit only finds known problems. A clean npm audit does not prove a package is safe. It cannot catch a brand-new malicious package or a typosquat that nobody has reported yet.

Step 3: Use lockfiles and reproducible installs

A lockfile (package-lock.json, poetry.lock, uv.lock, or a pinned requirements.txt) records the exact version of every package, including transitive ones. Without it, npm install may fetch a newer version tomorrow than it did today, so a surprise release can reach your production build.

Rules: commit the lockfile to Git, and in CI and production builds install from it exactly.

Terminal
# Node: install exactly what the lockfile says
# (fails if package.json and the lockfile disagree)
npm ci

# Python: freeze the exact versions you have tested
pip freeze > requirements.txt

npm ci never rewrites the lockfile and is made for CI. pip freeze writes lines like fastapi==0.115.0 for every installed package.

For Python you can go one step further with hash checking. Each package file gets a fingerprint, and pip refuses a file whose hash differs. The pip-tools package generates the hashes for you.

Terminal
pip install pip-tools

# requirements.in holds only your direct dependencies, one per line (for example fastapi)
pip-compile --generate-hashes requirements.in -o requirements.txt

# Install and verify hashes
pip install --require-hashes -r requirements.txt

requirements.txt now lists every package (including transitive ones) with --hash=sha256:... lines. If a package file is swapped, the install fails.

Step 4: Vet a package before you add it

Prompt · Review a package before installing
I am about to add the npm package [package-name] to a [type of project, e.g. React website]. I only need it for [what you need]. Help me decide whether to add it. Tell me what to check on npmjs.com and GitHub (maintainers, last release, open security issues, download trend, install scripts, number of its own dependencies), and suggest whether I could do this with the standard library or a few lines of my own code instead. Do not claim facts about the package that you cannot know; tell me what to verify myself.

What to expect: A good answer gives a checklist you then follow yourself. Treat any claim about a specific package as unverified until you check the registry page.

Before adding a dependency

  1. The name is spelled exactly as in the official documentation (copy the install command from the project's own site, not from a random blog).
  2. It has a real history: several releases, a linked source repository, an active maintainer.
  3. You really need it: is a package worth it for a 3-line function?
  4. It has few dependencies of its own (npm view <name> dependencies).
  5. It has no suspicious install scripts (npm view <name> scripts).
  6. The last release is recent enough that bugs get fixed.
Terminal
# See metadata without installing anything
npm view express version maintainers dependencies
npm view express scripts

# Install without running install scripts (a safer first look at an unknown package)
npm install some-package --ignore-scripts

npm view only reads the registry. --ignore-scripts stops preinstall and postinstall scripts from running. Some legitimate packages need them, so use it for a first look, not blindly everywhere.

AI coding assistants add a new twist: they sometimes suggest package names that do not exist. An attacker can register such a name and wait for someone to install it. Always confirm that a package an AI suggests exists, is the popular one, and does what you asked. More on this in *AI security: prompt injection, data leaks and model risks*.

Step 5: Automate updates and checks

Doing this by hand once is good. Doing it automatically is better. On GitHub, Dependabot opens pull requests when a dependency has a new version or a security fix. Create this file in your repository:

.github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "pip"
    directory: "/"
    schedule:
      interval: "weekly"
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Dependabot checks weekly and opens pull requests. Keep only the ecosystems your repository really uses. Your tests in CI then show whether each update is safe to merge.

.github/workflows/audit.yml
name: audit
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm audit --audit-level=high

Fails the build when a high or critical known vulnerability exists. It installs from the lockfile with npm ci so CI tests the same versions you ship.

Step 6: Know what you ship (SBOM)

An SBOM (Software Bill of Materials) is a list of every component in your software with versions, like the ingredients list on a food packet. When a new vulnerability is announced, companies with SBOMs can answer "are we affected?" in minutes instead of days. Common formats are SPDX and CycloneDX. Tools such as Trivy can generate one; you will try Trivy in *Security testing tools: ZAP, Semgrep and Trivy*. You do not need an SBOM for a college project, but you should know the word because customers and large employers often ask for them.

Mistakes to avoid

MistakeWhy it hurtsBetter habit
Ignoring the lockfileDifferent versions on every machineCommit it, use npm ci in CI
Running npm audit fix --force blindlyMay install breaking major versionsRead the plan, run tests
Using latest or * as a versionAny new release is trusted automaticallyUse ranges you understand plus a lockfile
Piping an installer script straight into bashRuns unreviewed code with your permissionsDownload, read, then run
Never updatingKnown bugs pile upSmall, regular updates

Try it: Audit your own project

Pick a project of yours (or clone any small open-source Node or Python project). 1) Run the audit command for its ecosystem. 2) Pick the most serious finding and write down the package, the fixed version, and whether your code uses the vulnerable feature. 3) Fix it, run the tests, and commit the updated lockfile. 4) Add dependabot.yml to the repository.

Try it: Spot the typosquat

Without installing anything, look up three package names on npmjs.com or pypi.org: requests, reqeusts and request. For each, note the download numbers, the first publish date and the repository link. Which would you trust and why?

Key takeaways

  1. Most of your code is other people's code, so dependencies are part of your attack surface.
  2. Run npm audit or pip-audit regularly, and judge each finding instead of panicking.
  3. Commit lockfiles and install from them exactly (npm ci, hashed requirements).
  4. Check a new package's name, history, maintainers, dependencies and install scripts before you add it.
  5. Automate: Dependabot for updates, an audit step in CI.
  6. Confirm that any package an AI assistant suggests really exists and is the right one.

Check yourself

Finishing the quiz marks the lesson as done.

1. Why is a lockfile committed to Git and used with npm ci in CI?

2. A package called reqeusts appears on a registry with few downloads and no source repository. What is it most likely to be?

3. npm audit reports no issues. What can you conclude?

4. What does an SBOM give a company when a new vulnerability is announced?

5. An AI assistant suggests npm install fast-json-safe-parse-pro. What should you do first?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free