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
| Problem | What happens | Example |
|---|---|---|
| Known vulnerability | A real package has a bug that attackers can exploit | An old version of a web framework with a public CVE |
| Typosquatting | A malicious package has a name close to a popular one | reqeusts instead of requests |
| Dependency confusion | A build picks a public package instead of your private one with the same name | Your internal package acme-utils replaced by a public acme-utils |
| Account takeover | An attacker steals a maintainer's login and publishes a bad version | A new patch release that quietly sends environment variables to a server |
| Install scripts | A package runs code the moment you install it | npm postinstall scripts |
| Abandoned package | Nobody fixes bugs any more | A 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
- 1Look at what you depend on (Step 1).
- 2Audit for known vulnerabilities (Step 2).
- 3Lock and pin exact versions (Step 3).
- 4Vet every new package before adding it (Step 4).
- 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.
# 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
pipdeptreenpm explain shows which of your dependencies pulled a package in. pipdeptree prints Python packages as a tree.
Step 2: Audit for known vulnerabilities
# 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.txtnpm audit ships with npm. pip-audit checks your Python packages against known advisories.
# 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 vulnerabilityRead 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.
# Apply fixes that do not need a major version change
npm audit fix
# Then run your tests and the app before committing
npm testnpm 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.
# 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.txtnpm 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.
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.txtrequirements.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
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
- 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).
- It has a real history: several releases, a linked source repository, an active maintainer.
- You really need it: is a package worth it for a 3-line function?
- It has few dependencies of its own (
npm view <name> dependencies). - It has no suspicious install scripts (
npm view <name> scripts). - The last release is recent enough that bugs get fixed.
# 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-scriptsnpm 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:
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.
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=highFails 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
| Mistake | Why it hurts | Better habit |
|---|---|---|
| Ignoring the lockfile | Different versions on every machine | Commit it, use npm ci in CI |
Running npm audit fix --force blindly | May install breaking major versions | Read the plan, run tests |
Using latest or * as a version | Any new release is trusted automatically | Use ranges you understand plus a lockfile |
| Piping an installer script straight into bash | Runs unreviewed code with your permissions | Download, read, then run |
| Never updating | Known bugs pile up | Small, 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
- Most of your code is other people's code, so dependencies are part of your attack surface.
- Run
npm auditorpip-auditregularly, and judge each finding instead of panicking. - Commit lockfiles and install from them exactly (
npm ci, hashed requirements). - Check a new package's name, history, maintainers, dependencies and install scripts before you add it.
- Automate: Dependabot for updates, an audit step in CI.
- 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.