Back to glossary

Attack Surface

An 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.

What is an attack surface?

#

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:

  • Entry points, where data or commands come in: HTTP routes, CLI arguments, file and image parsers, message queue consumers, webhooks.
  • Exit points, where data goes out: responses, logs, error messages, outbound requests.
  • The code and privileges behind them: whatever runs when an entry point is hit, and the permissions that code holds.

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.

How is an attack surface different from an attack vector?

#

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.

TermWhat it isSupply chain example
Attack surfaceEvery point an attacker can reach to enter, affect, or extract dataThe 65 packages and 34 npm accounts behind npm install express
Attack vectorThe path or technique used to get through one pointPhishing a maintainer to hijack their npm session
VulnerabilityA flaw at one point that makes an attack possibleA ReDoS bug in a header parser
ExploitInput or code that triggers a vulnerabilityA crafted header that pins the CPU
Security postureThe overall state of an organization's defenses across its surfaceWhether 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.

Dependencies add three attack surfaces

#

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 · Publish
Publisher surface
Every account, token, and CI workflow that can push a new version into your semver range.
Example
axios, March 2026: a hijacked maintainer session published 1.14.1 and 0.30.4.
Narrowed by
Trusted publishing, cooldowns, pre-install analysis
2 · Install
Install-time surface
Lifecycle scripts and implicit node-gyp builds. Runs on laptops and CI runners, next to their secrets.
Example
plain-crypto-js@4.2.1 postinstall dropper; Miasma binding.gyp waves
Narrowed by
Scripts off by default (npm v12, pnpm 10+)
3 · Run
Runtime surface
Dependency functions your app calls with untrusted input, plus code that runs at import.
Example
A CVE in a parser your request handler reaches
Narrowed by
Removing packages, reachability analysis, least privilege
The install stage is highlighted because it executes before any of your code or tests.

The 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.

Runtime surface

#

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.

Install-time surface

#

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.

Publisher surface

#

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.

How big is the attack surface of one npm install?

#

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 lockfilenpm accounts that own themSingle-owner packagesInstall scriptsInstalled packages referencing child_process
npm install express (5.3.0)65342600
npm install eslint (10.14.0)75484701
npm install jest (30.5.2)286116153115

The commands are short enough to rerun:

Bash
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             # 34

The 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:

JSON
"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.

How do you reduce your attack surface?

#

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.

ControlSurface it shrinksWhat it leaves open
Removing a dependencyAll three, for that package and its subtreeNothing for that package, but you now own the code
Install scripts off (npm v12, pnpm 10+)Install-timeCode that runs at import, and every package you approve
Lockfile with npm ciPublisher, for existing installsnpx, global installs, and new or updated packages
Cooldown (pnpm 11 default: 24 hours)Publisher, during a version's first dayCompromises that stay undetected longer than the window
Trusted publishing (OIDC)Publisher: stolen long-lived tokensHijacked maintainer sessions and poisoned CI workflows
Reachability analysisRuntime: CVE triageInstall-time and publisher surfaces; unused packages still install
Pre-install package analysisPublisher and install-time: new capabilities in new versionsAlerts 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.

Attack surface should be a diff, not an audit

#

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?