Back to glossary

Software Dependencies

Software dependencies are packages written by others that your code needs to build or run, declared in a manifest and installed by a package manager.

Also known as dependency, software dependency, third-party dependencies, open source dependencies, package dependencies, library dependencies, deps

Software dependencies are packages, libraries, and tools, written by someone else, that your code needs to build, test, or run. They are also called third-party or open source dependencies. You declare them in a manifest, and a package manager resolves each one to an exact version, downloads it, and executes it with your privileges.

We scaffolded the default Next.js starter on October 17, 2026. Its package.json declares 11 dependencies. npm 12.2.0 installed 358 packages, maintained by 243 distinct npm accounts and holding about 3.06 million lines of JavaScript, TypeScript, and type definitions. The app's own pages, styles, and config files total 165 lines. That ratio is the core fact about software dependencies: nearly all of the code you ship, and most of the code that runs on your laptop and in CI, was selected by manifests you never read. Our position is that a dependency is a grant of code execution, not a reference to a library. It should be judged on what its code does, and the check should happen before install, not after deploy.

What are dependencies in software?

#

Dependencies in software are code your program calls but does not contain. When a Next.js page runs import { useState } from "react", the module resolver does not find React in your repository. It walks up the directory tree to node_modules/react and loads whatever files a package manager put there. The dependency relationship has three layers, and each one is a different artifact:

  1. A declaration in the manifest: a name and a version constraint.
  2. A resolution in the lockfile: one exact version, a download URL, and a hash.
  3. The installed files: whatever the tarball contained, plus anything its scripts did.

The declaration is the part developers write. This is the complete dependency section of the starter's manifest, as generated by create-next-app on October 17, 2026:

JSON
"dependencies": {
  "next": "16.4.2",
  "react": "19.3.0",
  "react-dom": "19.3.0"
},
"devDependencies": {
  "@tailwindcss/turbopack": "^4",
  "@types/node": "^20",
  "@types/react": "^19",
  "@types/react-dom": "^19",
  "eslint": "^9",
  "eslint-config-next": "16.4.2",
  "tailwindcss": "^4",
  "typescript": "^5"
}

Values like ^4 are semantic versioning ranges, not versions. ^4 means any stable 4.x release, including one published a minute ago. The package manager turns each range into a concrete answer by querying the registry, and records that answer in the lockfile. Here is the lockfile entry npm wrote for next, trimmed to the fields that matter:

JSON
"node_modules/next": {
  "version": "16.4.2",
  "resolved": "https://registry.npmjs.org/next/-/next-16.4.2.tgz",
  "integrity": "sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==",
  "dependencies": {
    "@next/env": "16.4.2",
    "@swc/helpers": "0.5.23",
    "baseline-browser-mapping": "^2.9.19",
    "caniuse-lite": "^1.0.30001579",
    "postcss": "8.5.23",
    "styled-jsx": "5.1.6"
  },
  "optionalDependencies": {
    "@next/swc-darwin-arm64": "16.4.2",
    "@next/swc-linux-x64-gnu": "16.4.2",
    "...": "6 more platform builds",
    "sharp": "^0.35.5"
  },
  "peerDependencies": {
    "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
    "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0"
  }
}

Each field answers a different trust question. resolved says where the code comes from. integrity is a SHA-512 hash of the tarball, which npm checks on every later install, so the bytes cannot silently change. The three dependency maps say what next itself needs, and the package manager repeats the whole process for each of those entries until it reaches packages with no dependencies.

  1. 1

    Declare

    package.json
    "tailwindcss": "^4"
    Trustsevery future 4.x release
    Fails whena malicious version lands inside the range
    Control: release-age cooldown
  2. 2

    Resolve

    registry → lockfile
    "version": "4.x.y", "resolved": "…/tailwindcss-4.x.y.tgz"
    Truststhe newest matching version is the intended one
    Fails whena version published minutes ago wins
    Control: cooldown, committed lockfile
  3. 3

    Fetch

    tarball → node_modules
    "integrity": "sha512-…"
    Truststhe bytes match what was published
    Fails whenwhat was published is itself malicious
    Control: hash check (protects bytes, not intent)
  4. 4

    Execute

    install scripts, then every import
    install scripts (if any) · import "tailwindcss"
    Truststhe code does only what its README says
    Fails whenit reads secrets, spawns a shell, or calls home
    Control: analyze package contents before install
Figure 1. One dependency, four stages. Each stage trusts something different, and each control covers only its own stage.

The diagram traces one dependency through four stages, each with its own assumption and its own failure. At the declare stage, "tailwindcss": "^4" in package.json assumes every future 4.x release is acceptable; it fails when a malicious version lands inside the range. At the resolve stage, the package manager asks the registry for the newest matching version and writes it to the lockfile; it assumes the registry's newest version is the intended one, and a version published minutes earlier wins. At the fetch stage, the tarball is downloaded and checked against the integrity hash; this guarantees the bytes match what was published, not that what was published is safe. At the execute stage, install scripts may run immediately and the code runs again whenever it is imported; the assumption is that the code does only what its README says. A release-age cooldown narrows the first two stages, the lockfile and hash protect the third, and only analysis of the package contents addresses the fourth.

Direct and transitive dependencies

#

A direct dependency is one you declared. A transitive dependency is one that arrived because something you declared needs it. In the Next.js starter, the 3 declared runtime dependencies became 22 installed runtime packages, and the 8 declared development dependencies became 336 installed development packages.

The lockfile is larger than node_modules. It records 437 packages, because 79 of them are platform-specific builds and WebAssembly fallbacks, such as @next/swc-darwin-arm64, which npm skips on a Linux x64 machine. Several of the installed ones are not source code at all. @next/swc-linux-x64-gnu is a 97 MB compiled binary, and @img/sharp-libvips-linux-x64 ships a prebuilt native image library. Nobody reviews those as source, because there is no source in the package to review.

Every one of those packages is also a set of people. The 358 installed packages list 243 distinct maintainer accounts in their registry metadata, and any of those accounts can publish a version that the next unpinned resolution will select.

What types of software dependencies are there?

#

Software dependencies are grouped by when they are needed: at runtime, only during development, only during the build, or only on some platforms. Every major ecosystem draws these lines, with different names:

KindnpmPythonMavenCargoGo
Runtimedependencies[project] dependenciescompile, runtime scope[dependencies]require in go.mod
Development and testdevDependencies[dependency-groups] (PEP 735)test scope[dev-dependencies]Same require list
Build-time onlyTools in devDependenciesBuild backend in [build-system] requiresPlugins in <build>[build-dependencies]None
Provided by the hostpeerDependenciesNoneprovided scopeNoneNone
OptionaloptionalDependencies[project.optional-dependencies] (extras)<optional>true</optional>optional = true plus featuresNone

The labels describe when code is needed, not how much it is trusted. The npm docs define devDependencies as tools installed when you run npm install "from the root of a package". That root is a developer laptop or a CI runner, which is exactly where npm tokens, SSH keys, and cloud credentials live. In our starter, 336 of the 358 installed packages, 94%, are development dependencies.

Attackers know this. The Nx compromise on August 26, 2025 hit a build tool, the kind of package projects keep in devDependencies. The maintainers' advisory says the malicious versions carried "a postinstall script that scanned user's file system for text files." The payload stole GitHub tokens, npm tokens, SSH keys, and .env secrets, and public data showed more than 1,000 victim accounts with attacker-created exfiltration repositories. None of that code ever had to reach production.

Two other kinds deserve a note. npm installs peerDependencies automatically: the docs state, "As of npm v7, peerDependencies are installed by default." And an optional dependency that fails to build does not fail the install, which is why platform binaries ship that way.

When does dependency code run?

#

Dependency code can run at three moments: when it is installed, when it is built, and when it is imported. Install time is the dangerous one, because it requires no action beyond adding the package, and it runs before any test, review, or deploy gate. Almost every worm and credential stealer that hit npm since late 2025 ran at install time. As of October 2026, ecosystems differ sharply:

EcosystemCode from dependencies runs at install?MechanismDefault as of October 2026
npm 11 and earlierYespreinstall, install, postinstall, implicit node-gyp rebuildOn
npm 12Only if allowedSame, gated by allowScriptsOff for dependencies
pnpm 10 and laterOnly if allowedLifecycle scripts, gated by an allowlistOff for dependencies
pipYes, for source distributionssetup.py or the build backend runs to produce metadata and a wheelOn unless --only-binary :all:
CargoAt build, not fetchbuild.rs build scriptsOn
GoNoNo install or build hooksNot possible by design

The Go team states the design goal plainly: "neither fetching nor building code will let that code execute." The Go blog is careful to add that this is not a full sandbox, because any imported package can still run code in an init function once your program starts.

pip sits at the other extreme. We built a source distribution whose setup.py writes a file, and asked pip 26.2.1 only to plan an install:

Bash
$ cat demo_pkg/setup.py
import os, getpass
from setuptools import setup
open("/tmp/ran-at-install.txt", "w").write(f"setup.py ran as {getpass.getuser()} in {os.getcwd()}\n")
setup(name="demo-pkg", version="0.1.0")

$ pip install --dry-run --no-index --find-links dist --no-build-isolation demo-pkg
  Preparing metadata (pyproject.toml): finished with status 'done'
Would install demo-pkg-0.1.0

$ cat /tmp/ran-at-install.txt
setup.py ran as root in /tmp/pip-install-i2ih9t19/demo-pkg_0b698959d49f419ba6c0ad20cc391b98

A dry run executed the package's code, because pip needs the metadata, and for an sdist the metadata comes from running setup.py. pip download did the same. Only --only-binary :all: refused, by refusing the sdist altogether. The --no-index and --no-build-isolation flags only keep the demo offline. Without them, pip first downloads the build backend from PyPI into an isolated environment, then runs the same code.

npm's postinstall scripts were the same design for most of npm's life. npm v12 turned them off by default in July 2026, following pnpm 10 in January 2025. Installing the Next.js starter with npm 12.2.0 shows the new default:

Bash
$ npm install
added 358 packages, and audited 359 packages in 40s
...
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts   unrs-resolver@1.12.2 (postinstall: node postinstall.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.

One package out of 358 wanted to run code at install. Its script calls napi-postinstall, which describes itself as a helper "for handling native bindings in legacy npm versions." npm had already installed the correct native binding as an optional dependency, and eslint ran normally with the script blocked. That is the general pattern: a handful of packages need install scripts, and every project pays for the default with exposure.

The exposure in this case is not hypothetical. On July 18, 2025, an attacker holding a phished maintainer token published napi-postinstall@0.3.1 with a Windows payload, as part of the eslint-config-prettier compromise. At the time, unrs-resolver declared napi-postinstall: ^0.3.0, a range that admitted the malicious version while it was on the registry. The starter now resolves 0.3.4, but under npm 11 defaults, the same one-line postinstall would run whatever version a fresh resolution selected. We think default-on install scripts were a design mistake, and npm 12 and pnpm 10 have now corrected it.

Blocking install scripts does not make a malicious dependency safe. It removes the zero-click path. The same code still runs the first time your app, your test runner, or your build tool imports it.

How do you audit software dependencies for security?

#

To audit software dependencies, inventory the resolved tree, check it against known advisories, prioritize by reachability, and then inspect what each package actually does before it is installed. Each step answers a different question, and each misses something the others catch.

Inventory the resolved tree, not the manifest. The manifest lists 11 names. The lockfile lists 437. npm ls --all, pnpm list --depth Infinity, pip freeze, cargo tree, and go list -m all print what actually resolved. A software bill of materials (SBOM) is the same inventory in a portable format. An inventory covers what is in the project. It does not cover what npx, global installs, or CI tooling fetched outside the lockfile.

Check known vulnerabilities, then triage by reachability. On the fresh starter, npm audit reported this:

Bash
$ npm audit
braces  *
Severity: high
braces vulnerable to stack-exhaustion denial of service through deeply nested patterns - https://github.com/advisories/GHSA-vfj7-8cjw-p6xm
fix available via `npm audit fix --force`
Will install eslint-config-next@14.2.35, which is a breaking change
node_modules/braces
  micromatch  >=0.2.0
  ...
        eslint-config-next  >=14.3.0-canary.0
        Depends on vulnerable versions of @next/eslint-plugin-next

5 high severity vulnerabilities

All 5 "high" findings are one advisory, counted once per package on the path. GHSA-vfj7-8cjw-p6xm, published September 18, 2026 with a CVSS 4.0 score of 8.7, describes a stack overflow when braces expands deeply nested brace patterns. In this app, braces only expands glob patterns from the developer's own lint configuration. No attacker controls that input. The suggested fix would downgrade the lint config two major versions. This is what most CVE alerts look like: real bugs, unreachable in context. Reachability analysis, not the severity score, should decide which findings get engineering time.

Inspect behavior, because malware has no CVE. The axios compromise in March 2026 is the clearest example. The attacker published axios 1.14.1 and 0.30.4 with one new dependency, plain-crypto-js@4.2.1. Its postinstall hook ran setup.js, a 4,209-byte dropper that fetched a remote access trojan. Then it deleted itself and swapped in a clean package.json. Afterward, the installed node_modules/plain-crypto-js/ directory looked like a normal copy of crypto-js. A vulnerability scanner had nothing to match, and an audit of node_modules after the fact found nothing. The signals were in the package contents before install: a new dependency in a patch release, an install script, obfuscated strings, shell and network access. Socket's alerts flag those behaviors per package, including install scripts, network access, shell access, and obfuscated code. To enforce the check at install, sfw npm install runs the package manager behind Socket Firewall, a local proxy that refuses to fetch packages known to be malicious, including transitive ones. The free version blocks only human-confirmed malware, and it cannot block artifacts already in the local cache.

Check provenance, but do not stop there. Package provenance is a signed statement linking a published tarball to the source commit and build workflow that produced it. In our starter, 86 of the 358 installed packages, 24%, carry npm provenance attestations. Provenance answers "where was this built?" It does not answer "is this code safe?" The worm waves that hit npm between late 2025 and mid-2026 published from compromised maintainer accounts through legitimate pipelines, some with valid provenance.

MethodWhat it catchesWhat it missesVerdict
Inventory (lockfile, SBOM)What is installed and from whereAnything resolved outside the project; what the code doesRequired baseline
Known-vulnerability scan (npm audit, pip-audit, osv-scanner)Published advisoriesMalware, unreachable findings mixed with real onesNecessary, noisy without reachability
Reachability analysisWhich advisories your code can actually triggerInstall-time code, which runs whether or not you call itUse it to triage every scan
Behavioral analysis of package contentsMalware, install scripts, new capabilities, obfuscationLogic bugs that look like normal codeThe only control here that stops novel attacks
Provenance and signaturesTampering between source and registryMalicious code published by a legitimate maintainer or pipelineNecessary, not sufficient

Verdict: run the vulnerability scan and inventory continuously, but treat them as hygiene. The checks that stop supply chain attacks are the ones that inspect a package version's contents before it runs, and they have to run on every new version, because every update is a new trust decision. A dependency cooldown buys time for that inspection by refusing versions newer than a set age.

Adding a dependency is a one-command decision with no review

#

Russ Cox, who led the design of Go modules, put the problem in one line in 2019: "Software dependencies carry with them serious risks that are too often overlooked." Seven years later, the tooling still makes the risky part free. On October 17, 2026, npm install jest took 16 seconds and added 278 packages maintained by 171 distinct npm accounts. Nothing in the command asked what those packages are allowed to do. Package managers have started to refuse install-time execution by default. None of the mainstream ones lets you declare what each dependency may do at runtime, such as no network access, no shell, and no reading of environment variables, and then enforce it. Until they do, the capability check happens outside the package manager, or it does not happen at all.