# Application Security (AppSec)

> Application security (AppSec) is the practice of finding, fixing, and preventing exploitable weaknesses in software, including the third-party code it ships.

Also known as: software application security

Source: https://socket.dev/glossary/application-security

Last updated: 2026-11-06

Application security (AppSec) is the practice of finding, fixing, and preventing weaknesses that let attackers make software act against its authors' intent, across design, code, dependencies, build, and production. Because most of a modern application is third-party code, application security now depends as much on what the package manager installs as on what your team writes.

## What is application security?

Application security is the set of design decisions, tests, and runtime controls that keep an application's behavior inside the bounds its authors intended. Every application takes untrusted input, such as HTTP requests, uploaded files, and queue messages. Every application also holds something an attacker wants: database credentials, customer records, cloud tokens, a foothold on the network. An application vulnerability is any path from the first to the second.

For two decades the field organized itself around one source of those paths: bugs in code the team wrote. SQL injection, cross-site scripting, and broken access control are the canonical examples. The tooling grew up around them. [Static application security testing (SAST)](https://socket.dev/glossary/static-application-security-testing) reads source code, [dynamic application security testing (DAST)](https://socket.dev/glossary/dynamic-application-security-testing-dast) attacks the running app, and the [OWASP Top 10](https://socket.dev/glossary/open-web-application-security-project-owasp-top-10) ranks the bug classes worth testing for. Together these make up the discipline of [application security testing (AST)](https://socket.dev/glossary/application-security-testing-ast).

The second source is code the team imported, and it carries two different risks. A vulnerable dependency is an honest mistake: a bug with a CVE identifier that a scanner can match against your lockfile. A [malicious package](https://socket.dev/glossary/malicious-package) is an attack. It was written to run on your machines, and it has no CVE because nothing about it is accidental.

We think an application security program built only on first-party bug finding and CVE lists is blind to the attacks that have defined open source since 2025. The OWASP Top 10:2025 now agrees. It places [Software Supply Chain Failures at A03](https://owasp.org/Top10/2025/0x00_2025-Introduction), third of ten, widening the 2021 list's "Vulnerable and Outdated Components" to cover compromise of the supply chain itself. OWASP reports that the category "was overwhelmingly voted a top concern in the community survey." It also has the fewest occurrences in OWASP's test data, which OWASP attributes to how hard it is to test for. The risk practitioners rank near the top is the one their existing tests rarely detect.

## Where an application's code actually comes from

In a JavaScript application, most of the code was written by people outside the team, and the gap starts on day one. We measured it on the smallest real web application we could write: a four-line Express 5 server.

```javascript
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('hello'))
app.listen(3000)
```

```sh
$ npm init -y && npm install express@5
$ wc -l app.js
4 app.js
$ node -p "Object.keys(require('./package-lock.json').packages).filter(Boolean).length"
65
$ find node_modules -type f \( -name '*.js' -o -name '*.cjs' -o -name '*.mjs' \) -print0 | xargs -0 cat | wc -l
30371
$ npm query ':root *' | node -pe 'JSON.parse(require("fs").readFileSync(0)).map(p=>p.name).join("\n")' \
    | sort -u | xargs -n1 npm owner ls | cut -d' ' -f1 | sort -u | wc -l
34
```

As of November 6, 2026, with Express 5.3.0, the results are stark. One direct dependency pulls in 64 [transitive dependencies](https://socket.dev/glossary/transitive-dependency), 65 packages in all. Those packages contain 30,371 lines of JavaScript, comments and blank lines included, against four lines of our own. The registry lists 34 distinct npm accounts as owners of those packages, and organization team grants can add publishers that `npm owner ls` does not show.

The ratio is the point. Our code is about 0.01% of the JavaScript that runs. Express declares every one of its dependencies with a caret range such as `^2.0.0`, so any of those 34 accounts can publish a version that the next fresh resolution picks up automatically. None of those accounts belong to us, and none of them appear in a SAST report.

## How does application security work across the software lifecycle?

Application security works by placing a control at each stage where code is designed, written, installed, built, deployed, or run, because each control can only see what exists at its stage. The figure below maps the common controls to the stage where they run.

> Figure 1. Where each application security control runs. The install stage is the first point where third-party code executes, on developer laptops and CI runners, and it is the stage most application security programs leave unguarded.
>
> The six stages of the software lifecycle, the application security controls that run at each stage, and the install stage where dependency code first executes

The stages, controls, and blind spots in the figure, as a table:

| Stage | Controls that run here | What the controls see | What attacker code can do here |
| --- | --- | --- | --- |
| Design | Threat modeling | Trust boundaries and data flows, before code exists | Nothing yet |
| Write code | SAST, code review, secret scanning | First-party source code | Nothing yet, unless a developer's editor or tooling is compromised |
| Install dependencies | Package behavior analysis, install-script allowlists, package firewalls | The contents of each package before or as it lands | Run install scripts with the developer's or CI runner's credentials |
| Build and package | CVE-based SCA, SBOM generation, provenance attestation | The resolved dependency list and build outputs | Run build steps and native compilation |
| Deploy and test | DAST, IAST, penetration testing | The running application from outside or inside | Run at import and startup |
| Run in production | RASP, WAF, logging and monitoring | Live requests and process behavior | Run with production privileges |

The install stage is the one that most application security programs leave empty, and it is where dependency code first executes. [Install scripts](https://socket.dev/glossary/postinstall-scripts) such as `preinstall` and `postinstall` run the moment a package lands on disk, on a developer laptop or a CI runner. Those machines hold secrets as valuable as anything in production: npm publish tokens, cloud credentials, SSH keys, GitHub tokens with write access.

Attackers have used exactly that window. In [the second Shai-Hulud wave](https://socket.dev/blog/shai-hulud-strikes-again-v2), which began on November 24, 2025, compromised packages added a `preinstall` loader called `setup_bun.js`. Socket's research team counted more than 500 affected packages and more than 700 affected versions. The loader launched a 10MB obfuscated payload in a detached background process, and the install completed normally. Socket's analysis notes that "All errors exit with code 0, suppressing npm warnings."

Shai-Hulud is the pattern, not the exception. Socket's report on [npm v12](https://socket.dev/blog/npm-12) observes that almost every worm and credential stealer to hit the npm registry since late 2025 ran at install time, before any application code loaded. By the time SAST runs on the next pull request, or DAST probes the next staging deploy, that payload has already finished.

## SAST vs DAST vs SCA: what each application security control sees and misses

Each application security control answers one question about one kind of code, and no control answers all of them. The table compares the controls teams most often weigh against each other.

| Control | Inspects | Runs | Catches | Blind to |
| --- | --- | --- | --- | --- |
| Threat modeling | Architecture and trust boundaries | Design | Missing controls, unsafe data flows | Implementation bugs |
| SAST | Your source code | Every pull request | Injection and unsafe API use in code you wrote | Third-party code, runtime configuration |
| DAST | The deployed app, over HTTP | Staging or production | Exploitable injection and misconfiguration | Source locations, dependency code, install-time behavior |
| IAST | The app from inside the process, during tests | Test runs | Confirmed flaws with code locations | Routes the tests never exercise |
| CVE-based SCA | Manifest and lockfile | Pull request and build | Dependencies with published CVEs | Malware with no CVE, unpublished flaws |
| Package behavior analysis | The published package's actual code | Before install | Install scripts, obfuscation, network exfiltration, typosquats | Bugs in your own code |
| RASP and WAF | Live traffic and process calls | Production | Exploit attempts against known patterns | Attacks that already ran at install time |

Verdict: run SAST and code review on every pull request for the code you write, and DAST or IAST against staging for anything internet-facing. For the code you import, run [software composition analysis (SCA)](https://socket.dev/glossary/software-composition-analysis) and package behavior analysis together. SCA tells you which known bugs you shipped, and behavior analysis tells you whether a package is an attack. The common mistake is treating CVE-based SCA as the supply chain answer, which leaves the install stage unguarded.

## Why does CVE scanning miss supply chain attacks?

CVE scanning misses supply chain attacks because a CVE describes an accidental flaw in a known version, and a malicious package is neither. A CVE-based scanner works by lookup: it reads the name and version of each dependency from the lockfile and matches them against an advisory database. A malicious version published an hour ago has no advisory entry. By the time one exists, its install script has run on every machine that installed it.

The structural fix is to stop running dependency code by default. pnpm [blocked lifecycle scripts by default in version 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default), released in January 2025. npm followed with npm v12, released July 8, 2026, which turns dependency install scripts off unless the project allows them. Default-on lifecycle scripts were a design mistake, and both package managers have now corrected it. Here is the npm 12.2.2 output when a dependency ships a `postinstall` script:

```text
$ npm install esbuild@0.25.0
added 2 packages, and audited 3 packages in 1s

found 0 vulnerabilities
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts   esbuild@0.25.0 (postinstall: node install.js)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
```

Read the first lines again: "found 0 vulnerabilities." The CVE audit and the install-script gate are separate checks, and only the second one stopped code from running.

The new default narrows the problem without closing it. `npm approve-scripts esbuild` writes `"esbuild@0.25.0": true` to the `allowScripts` field of `package.json`, pinned to the installed version, so the next version needs a fresh approval. The pin forces the question but does not answer it: Socket's npm v12 report points out that the approval tooling lists the command each script runs, not what the script does. The skip is also soft by default, so the install succeeds with only a warning. And code can run at import time or interpreter startup, where install-script controls do not apply.

Provenance does not close it either. Package provenance proves which pipeline built a package, not whether the code in it is safe. The same npm v12 report notes that the Shai-Hulud and [Miasma](https://socket.dev/blog/shai-hulud-descends-to-hades-miasma-pypi-wave) worm waves ran from compromised maintainer accounts "publishing through legitimate pipelines, some with valid provenance." Provenance and signatures are necessary, but they are not sufficient.

## What are application security best practices?

Application security best practices are the controls that cover each lifecycle stage, chosen with a clear view of what each one costs and misses. These are the ones we would put first, each with its trade-off.

1. **Threat model the build, not only the app.** Draw the trust boundary around the package registry, the CI runner, and developer laptops, not just the production service. [Threat modeling](https://socket.dev/glossary/threat-modeling) costs hours per system and goes stale as the system changes, and it finds no implementation bugs.
2. **Run SAST and code review on every pull request.** These catch injection and unsafe API use in code you wrote, while the author still remembers it. They are noisy, and they never read the 99.99% of code that came from the registry.
3. **Gate dependencies before they install.** Analyze what a package actually contains, not what its metadata claims: install scripts, network calls, obfuscated code, file access outside the package. Keep install scripts off and approve them per package. The cost is friction: native modules that skip their build fail at runtime instead of at install. Socket Firewall puts this check in the network path, [inspecting each package request before the package manager downloads it](https://docs.socket.dev/docs/socket-firewall-overview). A pre-install gate is only as good as its analysis: a payload that stays dormant, or that fetches its real stage from a remote server at runtime, can pass inspection.
4. **Delay brand-new versions.** A [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) skips versions published less than a set time ago, giving the ecosystem time to catch and remove malicious releases. [pnpm 11](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults) made a 24-hour minimum release age the default in May 2026. The cost is that urgent security fixes wait too, and a patient attacker can simply wait out the window.
5. **Triage CVEs by reachability, not severity score.** A critical CVE in a function your code never calls is not your most urgent problem. [Reachability analysis](https://socket.dev/glossary/reachability-analysis) traces whether your code can reach the vulnerable function. Socket's documentation reports that its [full application reachability cuts up to 90% of false positives](https://docs.socket.dev/docs/reachability-analysis); that tier runs through the Socket CLI or a GitHub Action. Static reachability can miss paths created by dynamic dispatch, and it says nothing about malware.
6. **Treat the lockfile as an install record, not an exposure test.** A lockfile makes existing installs deterministic. Code fetched by `npx` or a global install never appears in it. After [the axios compromise](https://socket.dev/blog/axios-npm-package-compromised), we showed that [a clean lockfile checked after an incident does not prove you were never exposed](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise).
7. **Give CI and developer machines least privilege.** Use short-lived tokens, OIDC trusted publishing, and separate runners for untrusted code, so a payload that does run finds little to steal. None of this stops a compromised maintainer whose own pipeline publishes the malicious version.

## The open problem: application security tests code, attackers target people

Application security tooling is built to evaluate artifacts: source files, HTTP responses, version numbers. The weakest point in the supply chain is not an artifact. It is the 34 accounts behind a four-line server, any of which can be phished, have a token stolen, or hand off a package to a stranger.

Turning off install scripts by default removed the cheapest execution path, and that change was overdue. The next gap is review at the level of a single version, because compromise arrives as one new version from one hijacked account. npm 12 already pins script approvals to exact versions, but a reviewer asked to approve the next esbuild release sees the same `node install.js` whether the file behind it is unchanged or a credential stealer. Until package managers and registries show what changed in a version and what it does when it runs, application security will keep scanning the four lines it can see and trusting the thirty thousand it cannot.
