1.14.1 and 0.30.4.node-gyp builds. Runs on laptops and CI runners, next to their secrets.plain-crypto-js@4.2.1 postinstall dropper; Miasma binding.gyp wavesAn attack surface is every point where an attacker can enter, affect, or extract data from a system, including the third-party code it installs and runs.
Also known as Threat surface, Software attack surface, Attack surface area
An attack surface, sometimes called a threat surface, is the set of points where an attacker can try to enter a system, affect it, or extract data from it. It includes every network endpoint, input parser, credential, and line of code that untrusted input or untrusted people can reach. In modern applications, the largest part is usually third-party dependencies.
The standard model, from Michael Howard's work at Microsoft and Pratyusa Manadhata and Jeannette Wing's at Carnegie Mellon, counts what an attacker can reach and weights each item by the damage it allows and the effort it takes to reach. Run a dependency through that model and it scores worse than almost anything you write yourself. Its code runs in your process with your privileges, which is maximum damage potential. One compromised maintainer account can change that code for every consumer at once, which is minimal effort.
We think most attack surface programs measure the wrong boundary. External scans see what the internet can see. They do not see the code inside your build, the scripts that run when you install it, or the accounts that can publish new versions of it. That gap matters: almost every worm and credential stealer that hit npm since late 2025 ran at install time, before any application code loaded.
An attack surface is everything an attacker can send input to, read output from, or run code through. NIST defines it as the points on a system's boundary where an attacker can try to enter, cause an effect, or extract data. In practice that is three things:
General guides also split the surface into digital, physical, and human parts. The human part is social engineering: phishing an employee is an attack on the human surface. For software, the useful split is the one above, because it tells you what to remove.
Howard's 2004 MSDN article gave the field its working premise. "The core tenet of ASR is that all code has a nonzero likelihood of containing one or more vulnerabilities," he wrote. ASR is attack surface reduction. If every line can hide a bug, the only bug-free code is code an attacker cannot reach. Windows Server 2003 applied the idea directly. IIS 6.0 was not installed by default, served only static files when installed, and made all dynamic content opt-in. A WebDAV flaw would hit only customers who had turned WebDAV on.
Manadhata and Wing formalized the model in a 2007 Carnegie Mellon report. A system's resources are its methods, channels, and data items. Each resource contributes its damage potential divided by the attacker's effort to reach it. A method that runs as root and accepts unauthenticated input contributes far more than one that runs unprivileged behind login. Their case study is the useful part. ProFTPD 1.2.10 measured 312.99 on the method dimension and Wu-FTPD 2.6.2 measured 392.33, at roughly 28,000 and 26,000 lines of code. Wu-FTPD had less code and more attack surface. Attack surface is not code size. It is what is reachable, by whom, and with what privilege.
An attack vector is the path or technique an attacker uses to get through one point on the attack surface. The surface is the set of doors. The vector is how someone gets through a door. Phishing a maintainer is a vector. The maintainer's npm account is a point on your surface. The terms around them are easy to blur, so here they are side by side.
| Term | What it is | Supply chain example |
|---|---|---|
| Attack surface | Every point an attacker can reach to enter, affect, or extract data | The 65 packages and 34 npm accounts behind npm install express |
| Attack vector | The path or technique used to get through one point | Phishing a maintainer to hijack their npm session |
| Vulnerability | A flaw at one point that makes an attack possible | A ReDoS bug in a header parser |
| Exploit | Input or code that triggers a vulnerability | A crafted header that pins the CPU |
| Security posture | The overall state of an organization's defenses across its surface | Whether CI blocks install scripts and enforces lockfiles |
Attack surface management tools inventory the surface. External attack surface management (EASM) scans from the internet for exposed hosts, ports, and certificates. Cyber asset attack surface management (CAASM) pulls asset inventories from internal APIs. Both are useful. Neither enumerates the packages inside a build artifact, the install scripts those packages run, or who can publish them. That part of the attack surface has to be measured from the dependency graph.
A dependency adds attack surface at three moments: when a new version is published, when it is installed, and when your application runs it. The earlier the stage, the fewer of your controls stand in the way.
1.14.1 and 0.30.4.node-gyp builds. Runs on laptops and CI runners, next to their secrets.plain-crypto-js@4.2.1 postinstall dropper; Miasma binding.gyp wavesThe figure shows the three stages in order. Stage 1, the publisher surface, is every account, token, and CI workflow that can push a new version into your semver range; the axios compromise in March 2026 entered here, and trusted publishing, cooldowns, and pre-install analysis narrow it. Stage 2, the install-time surface, is lifecycle scripts and implicit node-gyp builds that run on laptops and CI runners next to their secrets; the plain-crypto-js@4.2.1 postinstall dropper and the Miasma binding.gyp waves entered here, and turning install scripts off by default (npm v12, pnpm 10 and later) narrows it. Stage 3, the runtime surface, is dependency code your application calls with untrusted input, plus code that runs at import time; a CVE in a parser your request handler reaches is the typical case, and removing packages, reachability analysis, and least privilege narrow it.
The runtime surface is the dependency code your application actually calls with untrusted input. This is where CVEs live: a prototype pollution bug in a merge utility, a ReDoS in a header parser. Most of a dependency tree is not on this path. Socket's Dependency Reachability builds an import graph across the tree and, on average, marks 25 to 35 percent of transitive dependencies as never imported. Reachability analysis is the right tool for triaging vulnerabilities on this surface.
Reachability is the wrong tool for the other two. A package that is never imported still gets installed, and its install script still runs. Dead code shrinks the runtime surface and leaves the install-time surface exactly as large as the full tree.
The install-time surface is code that runs because a package was installed, before anything imports it. npm runs lifecycle scripts named preinstall, install, and postinstall from every dependency in the tree. It also has an implicit rule. Per the npm scripts documentation, if a package has a binding.gyp file and no install script, npm defaults its install command to node-gyp rebuild.
Install-time code runs on developer laptops and CI runners, which is where npm tokens, cloud credentials, and SSH keys live. It runs before your tests, your linters, or your code review see anything. The axios compromise used this stage: the malicious axios versions added plain-crypto-js@4.2.1, whose postinstall hook ran a dropper that fetched a platform-specific remote access trojan. The Miasma waves of June 2026 went further. They shipped a binding.gyp file instead of an install script, so packages looked script-free while still running code through node-gyp.
Default-on install scripts were a design mistake, and the package managers have now said so in code. pnpm 10.0.0 blocked dependency lifecycle scripts by default in January 2025. npm v12, released July 8, 2026, turned off dependency install scripts by default, including the implicit node-gyp rebuild.
The publisher surface is every identity that can put a new version of any package in your tree onto the registry. Semver ranges like ^1.13.5 mean a new version flows into fresh installs without anyone on your team touching a file. Each maintainer account, each long-lived publish token, and each CI workflow with publish rights is an entry point into your build. Maintainer account takeover is the attack on this surface.
The axios case shows why this surface resists configuration. The axios package had about 100 million weekly downloads on npm when it was hit in March 2026. Its maintainer confirmed that attackers posed as a company, got remote access to his machine, and hijacked his live npm and GitHub sessions. The same report notes that 2FA or OIDC-based publishing would not have stopped the release, because the attacker used the maintainer's own access. Provenance and signatures are necessary but not sufficient. The Miasma-family waves published from compromised maintainer accounts through legitimate pipelines, some with valid provenance.
The attack surface of one npm install is every package, maintainer, and install script it resolves. We measured three common installs on November 7, 2026 with npm 10.9.9 on linux-x64. Each ran in an empty directory with --ignore-scripts, so nothing executed. We counted distinct packages in the lockfile, current owners per package with npm owner ls, install scripts from the lockfile's hasInstallScript field, and installed packages whose JavaScript references child_process.
| Install (version, Nov 7, 2026) | Packages in lockfile | npm accounts that own them | Single-owner packages | Install scripts | Installed packages referencing child_process |
|---|---|---|---|---|---|
| npm install express (5.3.0) | 65 | 34 | 26 | 0 | 0 |
| npm install eslint (10.14.0) | 75 | 48 | 47 | 0 | 1 |
| npm install jest (30.5.2) | 286 | 116 | 153 | 1 | 15 |
The commands are short enough to rerun:
mkdir demo && cd demo && npm init -y >/dev/null
npm install express --ignore-scripts --no-audit --no-fund
# distinct packages in the tree
jq -r '.packages | keys[] | select(. != "") | sub(".*node_modules/"; "")' \
package-lock.json | sort -u | wc -l # 65
# packages that declare an install script
jq '[.packages[] | select(.hasInstallScript)] | length' package-lock.json # 0
# distinct npm accounts that can publish a package in the tree
jq -r '.packages | keys[] | select(. != "") | sub(".*node_modules/"; "")' \
package-lock.json | sort -u \
| xargs -P8 -I{} npm owner ls {} 2>/dev/null \
| cut -d' ' -f1 | sort -u | wc -l # 34The first row is the point. npm install express is one line in package.json. It adds 65 packages and 34 npm accounts that can publish a new version of something in the tree. Twenty-six of those packages have a single npm owner, so one compromised account reaches your build. Sixty-four of them are transitive dependencies that nobody on your team chose.
The jest row makes a second point. Development dependencies are not a smaller attack surface, just a different one. They never ship to production, but they run on the machines that hold publish tokens and deploy keys. Jest's tree is the only one of the three with an install script, from @parcel/watcher:
"node_modules/@parcel/watcher": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
"hasInstallScript": true,
"license": "MIT"
}The script, node scripts/build-from-source.js, is a fallback for platforms without a prebuilt binary. It is benign. It also uses the same mechanism as the axios dropper, and nothing in the lockfile distinguishes the two. Under npm v12 it is skipped until someone approves it.
These counts have limits. The child_process column is a static grep, not an analysis of what the code does. Owner counts come from npm owner ls, which can omit organization team members with publish rights, so treat them as a lower bound. The gap can be large: the latest manifest of ms, a dependency of express, lists 55 maintainer accounts, while npm owner ls ms lists 6. The counts move with every release; they describe these resolutions on this date.
You reduce an attack surface by removing what is reachable, then lowering the privilege of what remains. For a service, that means turning off unused features and endpoints, authenticating before parsing untrusted input, and running each component with the least privilege it needs. Howard's rule still holds: if most users do not need a feature, ship it off.
For dependencies, each control shrinks one of the three surfaces and leaves the others. A lockfile makes existing installs repeatable, but as our axios blast radius analysis shows, it does not cover npx, global installs, or newly added packages. A dependency cooldown refuses versions younger than a set age; pnpm 11 defaults to 1,440 minutes, or 24 hours. Cooldowns work because most malicious versions are short-lived. npm's own timestamps show plain-crypto-js@4.2.1 was live for about 3 hours and 26 minutes, so a 24-hour cooldown would never have resolved it. Trusted publishing replaces long-lived tokens with short-lived OIDC credentials.
| Control | Surface it shrinks | What it leaves open |
|---|---|---|
| Removing a dependency | All three, for that package and its subtree | Nothing for that package, but you now own the code |
| Install scripts off (npm v12, pnpm 10+) | Install-time | Code that runs at import, and every package you approve |
| Lockfile with npm ci | Publisher, for existing installs | npx, global installs, and new or updated packages |
| Cooldown (pnpm 11 default: 24 hours) | Publisher, during a version's first day | Compromises that stay undetected longer than the window |
| Trusted publishing (OIDC) | Publisher: stolen long-lived tokens | Hijacked maintainer sessions and poisoned CI workflows |
| Reachability analysis | Runtime: CVE triage | Install-time and publisher surfaces; unused packages still install |
| Pre-install package analysis | Publisher and install-time: new capabilities in new versions | Alerts need human review; novel obfuscation can evade static analysis |
The verdict: start with the two controls that remove surface outright. Delete dependencies you can replace with a few lines of your own code, and keep install scripts off with an explicit allowlist. Add a lockfile and a cooldown next, since both are configuration. The publisher surface is the one no setting closes, because a compromised maintainer looks legitimate to every identity check. What remains is inspecting what each new version contains before it installs.
That is the gap Socket targets. Socket statically analyzes each package version's code before install and raises alerts for capabilities such as Install scripts, Shell access, Network access, and Environment variable access. A patch release that suddenly reads environment variables and opens a socket has changed your attack surface, whatever its version number says. The analysis has costs. Legitimate packages trip these alerts, so someone has to review them, and static analysis can be evaded by sufficiently novel obfuscation.
Teams review code diffs line by line and approve lockfile diffs without reading them. A pull request that adds 40 packages, 12 new publishers, and an install script gets the same approval as a typo fix, because nothing renders those changes as changes. Coding agents now choose and install dependencies themselves, which makes the unread lockfile diff the default path into your build. We think every dependency change should show its attack surface delta the way bundle-size checks show bytes: packages added, publishers added, and capabilities gained. Until that is standard, most teams cannot answer the basic question this page started with: what can reach their system?
Manifest confusion is a mismatch between an npm package's registry manifest and the package.json in its tarball, two copies that tools trust inconsistently.
A software bill of materials (SBOM) is a machine-readable inventory of the components, versions, and dependency relationships inside a piece of software.
Software composition analysis (SCA) inventories the third-party code in an application and checks each component for vulnerabilities, licenses, and malware.