# 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

Source: https://socket.dev/glossary/software-dependencies

Last updated: 2026-10-17

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](https://socket.dev/glossary/semantic-versioning-semver) ranges, not versions. `^4` means any stable 4.x release, including one published a minute ago. The [package manager](https://socket.dev/glossary/package-manager) turns each range into a concrete answer by querying the registry, and records that answer in the [lockfile](https://socket.dev/glossary/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.

> Figure 1. One dependency, four stages. Each stage trusts something different, and each control covers only its own stage.
>
> Four stages of one npm dependency: declare, resolve, fetch, execute, with the trust assumption, failure, and control at each 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](https://socket.dev/glossary/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:

| Kind | npm | Python | Maven | Cargo | Go |
| --- | --- | --- | --- | --- | --- |
| Runtime | dependencies | \[project\] dependencies | compile, runtime scope | \[dependencies\] | require in go.mod |
| Development and test | devDependencies | \[dependency-groups\] (PEP 735) | test scope | \[dev-dependencies\] | Same require list |
| Build-time only | Tools in devDependencies | Build backend in \[build-system\] requires | Plugins in \<build\> | \[build-dependencies\] | None |
| Provided by the host | peerDependencies | None | provided scope | None | None |
| Optional | optionalDependencies | \[project.optional-dependencies\] (extras) | \<optional\>true\</optional\> | optional = true plus features | None |

The labels describe when code is needed, not how much it is trusted. The [npm docs](https://docs.npmjs.com/cli/v11/configuring-npm/package-json) 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](https://socket.dev/blog/nx-packages-compromised) on August 26, 2025 hit a build tool, the kind of package projects keep in `devDependencies`. The [maintainers' advisory](https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c) 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](https://socket.dev/blog/npm-12). As of October 2026, ecosystems differ sharply:

| Ecosystem | Code from dependencies runs at install? | Mechanism | Default as of October 2026 |
| --- | --- | --- | --- |
| npm 11 and earlier | Yes | preinstall, install, postinstall, implicit node-gyp rebuild | On |
| npm 12 | Only if allowed | Same, gated by allowScripts | Off for dependencies |
| pnpm 10 and later | Only if allowed | Lifecycle scripts, gated by an allowlist | Off for dependencies |
| pip | Yes, for source distributions | setup.py or the build backend runs to produce metadata and a wheel | On unless --only-binary :all: |
| Cargo | At build, not fetch | build.rs build scripts | On |
| Go | No | No install or build hooks | Not possible by design |

The Go team states the design goal plainly: "neither fetching nor building code will let that code execute." [The Go blog](https://go.dev/blog/supply-chain) 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](https://socket.dev/glossary/postinstall-scripts) were the same design for most of npm's life. [npm v12 turned them off by default](https://socket.dev/blog/npm-12) in July 2026, following [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) 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](https://socket.dev/blog/npm-phishing-campaign-leads-to-prettier-tooling-packages-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](https://github.com/advisories/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](https://socket.dev/glossary/reachability-analysis), not the severity score, should decide which findings get engineering time.

**Inspect behavior, because malware has no CVE.** The [axios compromise](https://socket.dev/blog/axios-npm-package-compromised) 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](https://socket.dev/alerts/installScripts), [network access](https://socket.dev/alerts/networkAccess), [shell access](https://socket.dev/alerts/shellAccess), and [obfuscated code](https://socket.dev/alerts/obfuscatedFile). To enforce the check at install, `sfw npm install` runs the package manager behind [Socket Firewall](https://socket.dev/blog/introducing-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](https://socket.dev/glossary/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](https://socket.dev/blog/npm-12).

| Method | What it catches | What it misses | Verdict |
| --- | --- | --- | --- |
| Inventory (lockfile, SBOM) | What is installed and from where | Anything resolved outside the project; what the code does | Required baseline |
| Known-vulnerability scan (npm audit, pip-audit, osv-scanner) | Published advisories | Malware, unreachable findings mixed with real ones | Necessary, noisy without reachability |
| Reachability analysis | Which advisories your code can actually trigger | Install-time code, which runs whether or not you call it | Use it to triage every scan |
| Behavioral analysis of package contents | Malware, install scripts, new capabilities, obfuscation | Logic bugs that look like normal code | The only control here that stops novel attacks |
| Provenance and signatures | Tampering between source and registry | Malicious code published by a legitimate maintainer or pipeline | Necessary, 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](https://socket.dev/glossary/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.](https://research.swtch.com/deps)" 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.
