# Behavior-Based Security

> Behavior-based security detects threats by what code, users, or devices actually do, such as spawning shells or reading secrets, not by known signatures.

Also known as: Behavioral analysis, Behavior-based detection, Behavioral security, Capability analysis

Source: https://socket.dev/glossary/behavior-based-security

Last updated: 2026-11-02

Behavior-based security, also called behavioral analysis or behavior-based detection, judges code, users, or devices by what they do, like spawning shells or reading credentials, rather than matching known signatures. For open source packages, it means extracting each version's capabilities from its code and flagging behavior that does not fit. It catches malware that has no signature yet.

The technique is older than package registries. Antivirus heuristics, endpoint detection and response (EDR), and user and entity behavior analytics (UEBA) all model normal activity and alert on deviations. Package ecosystems suit it unusually well, because a package's behavior is written down in a tarball that anyone can read before it runs. The strongest signal is not any single capability. It is a capability that appears for the first time in a new version.

On March 30, 2026, the [axios compromise](https://socket.dev/blog/axios-npm-package-compromised) pulled in plain-crypto-js@4.2.1, a copy of crypto-js that added a [postinstall](https://socket.dev/glossary/postinstall-scripts) hook, shell execution, and file deletion. Socket's automated detection flagged it at 00:05:41 UTC on March 31, six and a half minutes after publish. No [CVE](https://socket.dev/glossary/common-vulnerabilities-and-exposures-cve) existed for it, and none ever will: malware is not a vulnerability. Our position is that capability analysis of the registry artifact, before install, should be the default control for third-party code, with CVE matching treated as a separate problem.

## What is behavioral analysis in cybersecurity?

Behavioral analysis in cybersecurity is the general form of behavior-based security: it detects threats from observed actions instead of from known indicators. A signature-based tool asks whether this file, hash, or byte pattern has been seen in malware before. A behavior-based tool asks whether this process, account, or package is doing something it should not.

Three families of tools share the idea:

- **Endpoint behavioral analysis** watches process trees, file writes, and network connections on a host. A word processor spawning PowerShell is a classic detection, whatever the dropped file's hash.
- **Heuristic analysis** scores static features of code: packers, encrypted strings, imports of process-injection APIs. It flags a file that looks like malware before anyone has catalogued it.
- **Anomaly detection and UEBA** build statistical baselines for users, hosts, and service accounts, then score deviations such as a 3 a.m. login or a sudden bulk download. User behavior analytics (UBA) is the older, users-only form.

All three share a weakness. "Normal" is learned from history, so a brand-new entity has no baseline, and a patient attacker can become the baseline. Every behavioral system also trades detection rate against alert volume. Those two problems follow the approach into package security.

## How does behavior-based security work for open source packages?

Behavior-based security for open source packages works by reading what a package's code can do before the package runs. That matters because installing a package is code execution. `npm install` runs third-party code with the full privileges of your user, and neither the registry nor the runtime asks what that code needs.

Node.js added a permission model in v20.0.0 and marked it stable in v22.13.0 and v23.5.0. It is opt-in, applies to the whole process rather than to each package, and is explicit about its limits. The [Node.js documentation](https://nodejs.org/api/permissions.html) says the model "does not provide security guarantees in the presence of malicious code." Every dependency in a Node process inherits everything the process can do.

Install time is where this bites hardest. In a 2020 review of 174 malicious packages from npm, PyPI, and RubyGems, [Ohm et al.](https://arxiv.org/pdf/2005.09535) found that "most malicious packages (56%) start their routines on installation."

So the only reliable place to learn a package's behavior is its code. A **capability** is a privileged operation that the package's code can reach: opening a socket, spawning a child process, reading files, reading [environment variables](https://socket.dev/glossary/environment-variables), or evaluating strings as code. Extracting capabilities is a static analysis problem. Parse every file, resolve imports and `require` calls against the platform's built-in modules, record each call site, and read `package.json` for lifecycle hooks.

Socket exposes these capabilities as individual alerts on every package version. As of November 2026, the capability alerts and their published severities are:

| Socket alert | What triggers it | Severity | Ordinary legitimate use | Why malware wants it |
| --- | --- | --- | --- | --- |
| Install scripts | Scripts that run automatically when the package is installed or built | Medium | Compiling native addons, downloading platform binaries | Code runs on npm install with no import needed |
| Shell access | child\_process and calls such as exec and spawn | Medium | Build tools, CLIs, git wrappers | Download and launch second-stage payloads |
| Network access | fetch() or the net, dgram, dns, http, and https modules | Medium | HTTP clients, SDKs, telemetry | Exfiltrate data, reach command and control |
| Filesystem access | fs reads and writes | Low | Config loaders, bundlers, test runners | Read SSH keys, tokens, and wallet files |
| Environment variable access | process.env in Node.js, os.environ in Python | Low | Reading configuration | Harvest CI secrets and cloud credentials |
| Uses eval | eval() and similar dynamic execution | Medium | Template engines, some polyfills | Run code that never appears as source |
| Dynamic require | require() called with a computed value | Low | Plugin loaders, optional dependencies | Load modules whose names are hidden |
| Debug access | vm, reflection, and other debug or dynamic-execution features | Low | Test frameworks, REPLs, sandboxes | Execute code outside normal module loading |
| URL strings | External URL or IP fragments in the code | Low | API endpoints, documentation links | Hard-coded payload hosts and drop sites |

Severities come from Socket's public alert pages, such as [Network access](https://socket.dev/alerts/networkAccess). On Socket package pages, each alert links to the line of code that triggered it.

## Why a capability diff beats a capability list

A capability list on its own says little. Many popular packages legitimately touch the network, the filesystem, or the environment. The useful question is what a version can do that the previous version could not, and whether that fits what the package is for.

You can reproduce the first half of this yourself. The script below, `caps.mjs`, is a toy capability extractor of 65 lines. It parses every JavaScript file in an unpacked tarball with acorn, maps built-in module imports to capabilities, and flags `eval`, computed `require` calls, `process.env` reads, URL strings, and install hooks. A production analyzer also tracks aliasing, re-exports, data flow, and other ecosystems, but the principle is the same.

```javascript
// caps.mjs: a toy capability extractor for an unpacked npm package.
// usage: node caps.mjs <package-dir>
import fs from 'node:fs'
import path from 'node:path'
import * as acorn from 'acorn'
import * as walk from 'acorn-walk'

const CAP = {
  network: ['http', 'https', 'net', 'dgram', 'dns', 'tls', 'http2'],
  shell: ['child_process'],
  filesystem: ['fs', 'fs/promises'],
  debug: ['vm', 'inspector', 'v8'],
}
const modCap = {}
for (const [cap, mods] of Object.entries(CAP)) for (const m of mods) modCap[m] = modCap['node:' + m] = cap

const dir = process.argv[2]
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
const found = {} // capability -> Set of "file:line"
const hit = (cap, file, node) => (found[cap] ??= new Set()).add(`${file}:${node.loc.start.line}`)

for (const h of ['preinstall', 'install', 'postinstall']) {
  if (pkg.scripts?.[h]) (found.installScripts ??= new Set()).add(`package.json scripts.${h}`)
}

function files(d) {
  return fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => {
    const p = path.join(d, e.name)
    if (e.isDirectory()) return e.name === 'node_modules' ? [] : files(p)
    return /\.(c|m)?js$/.test(e.name) ? [p] : []
  })
}

for (const f of files(dir)) {
  const rel = path.relative(dir, f)
  let ast
  try {
    ast = acorn.parse(fs.readFileSync(f, 'utf8'), { ecmaVersion: 'latest', sourceType: 'module', locations: true, allowHashBang: true, allowReturnOutsideFunction: true })
  } catch {
    try { ast = acorn.parse(fs.readFileSync(f, 'utf8'), { ecmaVersion: 'latest', sourceType: 'script', locations: true, allowHashBang: true, allowReturnOutsideFunction: true }) } catch { continue }
  }
  const mod = (name, node) => modCap[name] && hit(modCap[name], rel, node)
  walk.full(ast, (n) => {
    if (n.type === 'ImportDeclaration') mod(n.source.value, n)
    if (n.type === 'ImportExpression' && n.source.type !== 'Literal') hit('dynamicRequire', rel, n)
    if (n.type === 'CallExpression') {
      const c = n.callee
      if (c.type === 'Identifier' && c.name === 'require') {
        const a = n.arguments[0]
        if (a?.type === 'Literal') mod(a.value, n)
        else hit('dynamicRequire', rel, n)
      }
      if (c.type === 'Identifier' && c.name === 'eval') hit('eval', rel, n)
      if (c.type === 'Identifier' && c.name === 'fetch') hit('network', rel, n)
    }
    if (n.type === 'NewExpression' && n.callee.name === 'Function') hit('eval', rel, n)
    if (n.type === 'MemberExpression' && n.object.type === 'MemberExpression' &&
        n.object.object.name === 'process' && n.object.property.name === 'env') hit('envVars', rel, n)
    if (n.type === 'Literal' && typeof n.value === 'string' &&
        /\bhttps?:\/\/(?!(www\.)?(w3\.org|github\.com|npmjs\.(com|org)))/.test(n.value)) hit('urlStrings', rel, n)
  })
}

const out = Object.fromEntries(Object.entries(found).map(([k, v]) => [k, { count: v.size, first: [...v].slice(0, 2) }]))
console.log(JSON.stringify({ package: `${pkg.name}@${pkg.version}`, capabilities: out }, null, 2))
```

Run it against two real packages from the npm registry. The output below is condensed to one object per package.

```bash
npm install acorn@8 acorn-walk@8
npm pack crypto-js@4.2.0 axios@1.14.0
mkdir -p crypto-js axios
tar xzf crypto-js-4.2.0.tgz -C crypto-js
tar xzf axios-1.14.0.tgz -C axios
node caps.mjs crypto-js/package
node caps.mjs axios/package
```

```text
{ "package": "crypto-js@4.2.0", "capabilities": {} }
{ "package": "axios@1.14.0", "capabilities": {
    "urlStrings": { "count": 7, "first": ["dist/axios.js:1937", "dist/axios.min.js:4"] },
    "network":    { "count": 6, "first": ["dist/node/axios.cjs:7", "dist/node/axios.cjs:8"] },
    "envVars":    { "count": 1, "first": ["dist/node/axios.cjs:2075"] } } }
```

crypto-js@4.2.0 reaches no privileged capability at all. Its only built-in import is `crypto`, for secure random numbers. axios@1.14.0 requires `http`, `https`, and `http2` on lines 7 to 9 of its Node build, and reads proxy settings from `process.env` on line 2075. Network access in an HTTP client is expected. Any capability in a pure-JavaScript crypto library is not.

The attacker behind plain-crypto-js understood baselines. Version 4.2.0 was published at 05:57 UTC on March 30, 2026, as a clean copy of crypto-js with no payload. Version 4.2.1 followed at 23:59:12 UTC, a patch bump 18 hours later. Both timestamps are still visible in the [npm registry document for plain-crypto-js](https://registry.npmjs.org/plain-crypto-js), even though npm has since replaced the package with a security placeholder. The clean release gave the name a history. The patch release was the attack.

> Figure 1. Capability profiles. Four signals appear in plain-crypto-js@4.2.1 that its clean baseline never had.
>
> Capability matrix comparing crypto-js 4.2.0, axios 1.14.0, and plain-crypto-js 4.2.1

The figure shows three package versions against seven capabilities. crypto-js@4.2.0 has none of them. axios@1.14.0 has network access, environment variable access, and URL strings, all consistent with an HTTP client. plain-crypto-js@4.2.1 has an install script, shell access, filesystem access, obfuscated code, and a computed module load. Its network traffic runs through `curl` in a spawned shell, so it appears as shell access rather than as a network import. Its command and control URL is encoded, so the URL strings check sees nothing. The plain-crypto-js column is reconstructed from Socket's published analysis of the malicious `setup.js`, because npm no longer serves the tarball.

| Capability | crypto-js@4.2.0 | axios@1.14.0 | plain-crypto-js@4.2.1 |
| --- | --- | --- | --- |
| Install script | No | No | Yes: postinstall runs node setup.js |
| Shell access | No | No | Yes: child\_process, launching curl, cscript, osascript, nohup |
| Filesystem access | No | No | Yes: deletes setup.js and package.json, renames package.md |
| Network access | No | Yes: http, https, http2 | Indirect, through curl in a shell |
| Environment variable access | No | Yes: proxy settings | Not reported |
| URL strings | No | Yes | Hidden: the command and control URL is encoded |
| Obfuscated code or computed module load | No | No | Yes: 18 encoded strings, including module names |

Four new signals appeared together in a patch release of a crypto library. That combination is a far stronger indicator than any one of them alone. It is also a pattern we [described in 2022](https://socket.dev/blog/inside-node-modules) as permission creep: attackers publish in patch versions to reach the most victims through caret ranges. Any project on `^1.14.0` or `^0.30.0` would have resolved the poisoned axios releases on its next install that re-resolved the range.

## How do attackers evade behavior-based security?

Attackers evade behavior-based security by hiding capabilities from static extraction, by moving execution to paths that scanners do not watch, and by erasing evidence before anyone looks. Each technique has a cost, and the cost is usually visible.

**Hiding capabilities.** The plain-crypto-js dropper never contains the literal string `child_process`. Socket's analysis found 18 strings, including the module names `child_process`, `os`, and `fs`, behind two layers of encoding: reversed base64, then an XOR keyed on `OrDeR_7077`.

```javascript
// Deobfuscation functions from setup.js, as reconstructed by Socket
const _trans_1 = function(x, r) {
  const E = r.split("").map(Number);
  return x.split("").map((x, r) => {
    const S = x.charCodeAt(0), a = E[7 * r * r % 10];
    return String.fromCharCode(S ^ a ^ 333);
  }).join("");
};

const _trans_2 = function(x, r) {
  let E = x.split("").reverse().join("").replaceAll("_", "=");
  let S = Buffer.from(E, "base64").toString("utf8");
  return _trans_1(S, r);
};
```

A scanner that only matches `require('child_process')` misses this. The hiding itself is still a behavior, though. [Code obfuscation](https://socket.dev/glossary/code-obfuscation), high-entropy strings, and a module name computed at runtime are signals in their own right, and Socket rates Obfuscated code as High severity, above any single capability alert. Legitimate libraries have little reason to encrypt the names of the built-in modules they load.

**Unwatched execution paths.** Scanners that check only `preinstall` and `postinstall` fields can be bypassed. The [Miasma worm's "Phantom Gyp" waves](https://socket.dev/blog/miasma-mini-shai-hulud-hits-leoplatform-npm-packages-go-ecosystem) added a `binding.gyp` file instead. npm runs `node-gyp` automatically when that file exists, and command expansion in the file executes JavaScript during build configuration. A behavior model has to cover every way the package manager runs code, not just the documented hook names. [npm v12](https://socket.dev/blog/npm-12) now blocks dependency lifecycle scripts and the implicit `node-gyp` rebuild by default, and [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) blocked lifecycle scripts by default before it. Both changes correct a default that should never have shipped.

**Anti-analysis.** Dynamic analysis, which detonates a package in a [sandbox](https://socket.dev/glossary/sandbox-environment) and records what it does, catches behavior that static analysis cannot resolve. Malware checks for sandboxes. The [ua-parser-js compromise](https://socket.dev/blog/inside-node-modules) of October 2021 looked up the victim's country by IP and exited early in Russia, Ukraine, Belarus, and Kazakhstan. Time bombs, CI-only triggers, and checks for analysis tooling all mean a clean detonation is not proof of a clean package.

**Anti-forensics.** After launching its payload, plain-crypto-js deleted `setup.js`, deleted its own `package.json`, and renamed a bundled `package.md` into place. That file was the clean 4.2.0 manifest with no `postinstall` hook. Anyone inspecting `node_modules` afterward found an ordinary copy of crypto-js.

> Figure 2. Observable evidence of plain-crypto-js@4.2.1 at each stage of install, from Socket's analysis.
>
> What evidence of plain-crypto-js 4.2.1 is visible at each stage of npm install

The figure follows `npm install plain-crypto-js@4.2.1` through six stages. In the registry tarball before install, every malicious artifact is present: the `postinstall` hook, `setup.js` at 4,209 bytes, and the encoded strings. During install, npm runs `postinstall`, which runs `node setup.js`. The dropper then decodes its strings, detects the operating system with `os.platform()`, and downloads a platform-specific second stage from `sfrclak[.]com:8000`. It deletes `setup.js` and `package.json` and renames `package.md` to `package.json`. After install, `node_modules/plain-crypto-js` looks like an ordinary copy of crypto-js, and its `package.json` reports version 4.2.0 with no install hook. Only the first stage gives a scanner complete evidence, which is why analysis has to happen before install.

**Indirection.** The compromised axios releases changed almost nothing in axios itself. Socket's analysis found a minimal change that primarily added one new entry to `dependencies`. The new behavior lived one hop down, in a new [transitive dependency](https://socket.dev/glossary/transitive-dependency). The publish was anomalous too: axios@1.14.1 had no matching tag in the axios GitHub repository, a break from the project's normal release workflow that Socket called out in its first report. Behavior-based security for packages has to run over the resolved dependency tree, not over the package a developer typed.

## Behavior-based security vs signature-based detection

Signature-based detection and behavior-based security fail in opposite directions. Signatures are precise but blind to anything new. Behavioral signals see new attacks but fire on legitimate code. The useful comparison is by what each control can see, and when.

| Approach | Catches first-seen malware | When it acts | Main blind spot | Main cost |
| --- | --- | --- | --- | --- |
| Hash and pattern signatures (YARA, AV) | No | After someone catalogues the sample | Any new or re-packed payload | Low; precise once a signature exists |
| CVE and advisory matching | No | After disclosure and publication | Malware, which never receives a CVE | Low, but most CVE alerts are not reachable |
| Static capability analysis of the registry artifact | Yes | Before install | Heavy obfuscation, native code, logic resolved at runtime | False positives on legitimate capability use |
| Dynamic detonation in a sandbox | Yes, if triggered | Before install, if run on the registry artifact | Sandbox-aware, geo-fenced, or time-delayed payloads | Compute per version; slower verdicts |
| Runtime enforcement (Node permission model, EDR) | Partly | During execution | Code already running with your privileges; per-process, not per-package | Breakage, tuning, coverage gaps |
| Anomaly detection and UEBA on maintainers and publishing | Sometimes | At or after publish | New accounts with no baseline; attackers using legitimate pipelines | Noisy without context |

Our verdict: for third-party packages, static capability analysis of the registry artifact is the primary control. It is the only one that sees the complete package, before any of it runs, for every version. Use dynamic analysis to resolve what static analysis cannot, especially encoded strings and native code. Keep signatures and advisory matching for what they do well, known samples and known vulnerabilities. Treat runtime enforcement and EDR as the layer that limits damage once prevention has failed, not as prevention.

## What behavior-based security costs

The cost of behavior-based security is false positives. Many packages that open sockets are HTTP clients or SDKs, and many packages that read environment variables are reading configuration. A system that blocks on raw capabilities would block axios, as the scan above shows, along with a large share of ordinary dependencies.

Socket's [default security policy](https://docs.socket.dev/docs/security-policy-default-enabled-alerts) reflects that. As of November 2026, it blocks only Known Malware by default. Obfuscated code is set to warn, and the individual capability alerts, including Install scripts, are not enabled unless a team turns them on. Capabilities are evidence for a [malware detection](https://socket.dev/glossary/malware-detection) verdict. They are not verdicts by themselves.

The failure mode of skipping that step is now documented. On May 26, 2026, [OSV withdrew 157 malicious-package reports](https://socket.dev/blog/osv-withdraws-157-malware-reports) after automated detections flagged trusted npm and PyPI releases. The FastAPI record cited a new optional dependency as suspicious and identified no payload. A behavioral change was promoted to a [malicious package](https://socket.dev/glossary/malicious-package) record, and that record flowed into scanners and CI gates that treat OSV as an enforcement source. A capability or manifest change should open an investigation. Only a confirmed payload should turn it into a malware verdict.

The deeper limit is structural. A browser extension declares its permissions in a manifest, and the browser enforces them. An npm or PyPI package declares nothing, and Node's permission model applies to the whole process, so a date formatter and a database driver in the same process get the same rights. Until registries accept per-package capability declarations and runtimes enforce them, behavior-based security for open source remains inference from code. Done before install, it is very good inference, but the package still runs with everything you have.
