Skip to content

Runtime protection (RASP): stop exploits inside your app

Runtime protection (RASP, runtime application self-protection) runs inside your application, in the Shield360 SDK. The SDK keeps track of the untrusted input in the request it is serving. For AI apps, it also tracks the model output it has seen. It checks that input where it reaches a dangerous call (a sink): a SQL or MongoDB driver, a shell, the file system, an HTTP client, a deserializer, an XML parser or an HTML response.

Checks are structural. The SDK does not match a list of known attack strings. For example, a SQL injection is input that changes the shape of the query it lands in. A parameterised query never matches, because the input never becomes part of the query text.

  • Off until you turn it on in your code.
  • Monitor by default. You choose, per app and per attack type, what to block.
  • Fails open. An error inside a check never breaks your app. The only error runtime protection raises on purpose is its block error, which your framework middleware turns into a 403.
  • Your data stays in your app. Inputs stay in memory for the length of the request. Shield360 receives what kind of attack it was, where it happened and what was decided. It does not receive the input.

Runtime protection is part of the Security module. You change its settings on each app’s Security tab, under Runtime protection.

All four SDKs follow the same contract and pass the same shared test cases. They differ only in how they hook into each language.

Attack typeChecked atExamples
sql_injectionSQL drivers (dialect from the driver)pg.query and the drivers each SDK wraps
nosql_injectionMongoDB filters and updatesoperator objects that came from the request body or query
command_injection, shell_spawnShell commands and process startsNode exec/spawn with a shell, Python os.system/subprocess(shell=True), Go rasp.Command, .NET cmd /c and sh -c
path_traversal, file_writeFile reads, writes, deletes, renames and directory listingsfs.readFile, Python open
ssrf, outboundOutbound HTTP, on the address the connection actually usesNode socket lookup, Python getaddrinfo, Go DialContext, .NET SocketsHttpHandler.ConnectCallback
deserializationUnsafe deserializerspickle and marshal, PyYAML and js-yaml unsafe loaders, jsonpickle, node-serialize, funcster, .NET BinaryFormatter and TypeNameHandling
code_injectionCode evaluationeval, new Function, vm, Python eval/exec/compile
xxeXML parsers that resolve external entitieslibxmljs, xml2js, lxml, XmlReaderSettings. Python xml.etree and Go encoding/xml report xxe.doctype only
xssHTML responses (monitor only)reflected input in an HTML body
virtual_patchEach request, before your handler runsthe rules in Virtual patches

TypeScript and Python hook the libraries in place. Go and .NET use middleware, driver wrappers and the platform’s own hooks (DiagnosticListener and interceptors in .NET). Each SDK’s reference lists exactly which libraries it covers. Middleware for the common frameworks (Express, FastAPI, net/http, ASP.NET Core) captures each request’s input and turns a block into the 403 below.

Runtime protection is off until the app enables it:

SDKEnable
TypeScriptrasp: true
Pythonrasp=True
Gorasp.Enable(...)
.NETAddShield360Rasp()

You can also set the environment variable SHIELD360_RASP=monitor or SHIELD360_RASP=block.

When the SDK has a Shield360 URL and an API key, it fetches its app’s settings from Shield360 about once a minute and reports what it sees. The API key decides the environment (project database).

Any of these stops every check at once. They are listed in order of precedence:

  1. The environment variable SHIELD360_RASP=off on the app.
  2. The Kill switch on the app’s Runtime protection settings in Shield360. Protection on set to off has the same effect.
  3. The runtime call in your code (disableRasp(), shield360.rasp.disable(), rasp.Disable(), Shield360Rasp.Disable()).

A change you make in Shield360 reaches the app the next time it fetches its settings, usually within a minute.

Each attack type is off, monitor or block. Any attack type without its own mode uses the app’s default mode, which starts as monitor.

  • monitor: the call goes ahead and an event is recorded with decision monitored.
  • block: the call is refused, the request gets a 403, and an event is recorded with decision blocked.
  • off: the check is skipped.

xss is never blocked: block is treated as monitor. For virtual_patch, the mode caps each rule’s own action.

The block response:

HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error":"Request blocked by Shield360 runtime protection","attack":"sql_injection","rule":"sqli.userinput","event":"<event id>"}

Open Applications & agents, choose the app, then open the Security tab and go to Runtime protection. Anyone who can view Data protection can see the settings. Changing them needs the permission to manage policies, and every change is recorded in the audit log with the settings before and after.

SettingDefaultMeaning
Protection ononOff stops the app’s checks, the same as the kill switch.
Kill switchoffEmergency stop. Every check returns at once, whatever else is set.
Default modemonitorThe mode for every attack type that has no mode of its own.
Mode per attack typedefaultoff, monitor or block. XSS offers monitor only.
Allowed commandsnoneExecutable names the app may start during requests without a shell_spawn event.
Outbound allow-listnoneHost names or *.domain patterns. When the list is not empty, a call to any other host is an outbound event. The Shield360 URL and the OTLP endpoint are always allowed.
File rootsnoneAbsolute directories. A file path that holds input and lies outside every root is path_traversal.
Send matched-input snippetsoffSends at most 64 characters of the matched input, with credentials masked. Snippets are sent only if your organisation also stores content (Settings → Data retention). They are kept for 30 days at most, or less if you choose (see Retention).

The SDK receives these settings as a signed config: a compact JWS (typ: shield360-rasp+jws, EdDSA) signed with your environment’s attestation key, the same key that signs agent passports. The SDK checks the signature against the key you pinned, then the app name and the expiry, before it uses a config. If a config fails any check, the SDK keeps the last one it verified, or else its local options. Settings from Shield360 override the app’s local modes and allow-lists. A local off and the environment kill switch always win.

The signed config can turn protection off and widen allow-lists, so the SDK trusts it only under a key you give it yourself:

  1. Open the app’s Security tab, then Runtime protection → Settings. Under Config signing key, copy the JWK (or the JWKS, which lets you list an old and a new key during a rotation).
  2. Give it to the SDK as configPublicKey (TypeScript), config_public_key (Python), ConfigPublicKey (Go, .NET), or the environment variable SHIELD360_CONFIG_PUBLIC_KEY.
Terminal window
export SHIELD360_CONFIG_PUBLIC_KEY='{"kty":"OKP","crv":"Ed25519","x":"<from the settings>","kid":"<from the settings>"}'
  • Without a pinned key the SDK does not use the signed config. It runs its built-in defaults in monitor mode for every attack type, and logs a warning once.
  • Trust on first use (pinning the first key the platform sends) is for development only. It needs the explicit opt-in allowUnpinnedConfig or SHIELD360_CONFIG_TOFU=dev, and the SDK ignores it when the app runs in production.
  • The keys field of the config response is informational. The SDK never trusts it in place of a pin.
Terminal window
curl -sS "https://app.shield360.ai/api/rasp/config?app=orders-api" \
-H "Authorization: Bearer $SHIELD360_API_KEY"

The response is { "config": "<JWS>", "keys": [ <JWK> ] }. The endpoint honours If-None-Match and answers 304 when nothing has changed. If your organisation does not hold the Security module, the config carries the defaults: monitor every attack type, no allow-lists and the SDK’s built-in virtual patches. It never carries the kill switch.

A virtual patch blocks the published exploit shape of one known vulnerability before your handler runs. This gives you protection while you upgrade. Shield360 ships these in every SDK and in the signed config:

RuleProtects againstCVE
vp.react2shellReact Server Components Flight deserialization RCE (React2Shell), through a Next.js server actionCVE-2025-55182, CVE-2025-66478
vp.react2shell-flightThe same flaw, without a Next-Action headerCVE-2025-55182
vp.next-middleware-bypassNext.js middleware authorization bypass with x-middleware-subrequestCVE-2025-29927
vp.node-serialize-iifenode-serialize unserialize() code executionCVE-2017-5941
vp.node2shell-brotliNode.js Brotli decoder out-of-bounds write (Node2Shell). It refuses Brotli-encoded request bodies on unpatched Node.js 22, 24 and 26CVE-2026-111497
vp.example-test-headerNot a real advisory. It matches the header x-shield360-rasp-test: exploit, so you can test the pipeline from end to endn/a

Virtual patches are the only place runtime protection matches request signatures, and each one is the documented request shape from a public advisory. The Node2Shell rule is based on third-party reports of the fixed releases (22.23.4, 24.22.1, 26.12.1). Upgrading Node.js is the fix.

Once per process, shortly after it starts, the SDK checks how your app is set up and reports each risky setting as a Security baseline event. These are fixes for the app’s owner, so they never block. Each rule reads only a flag, a version or an id. No setting’s value is ever sent.

RuleMeans
baseline.tls_verify_offCertificate checks are off for every outbound connection.
baseline.inspector_in_productionA debugger is listening, or was requested, in production.
baseline.development_mode_in_productionThe app runs in development mode while its environment says production.
baseline.running_as_rootThe process runs as root or an elevated administrator.
baseline.config_key_unpinnedIn production, the signed config key isn’t pinned.
baseline.runtime_end_of_lifeThe runtime’s release line no longer gets security fixes.

The TypeScript SDK runs these checks today. The other SDKs add their own equivalents with their sink hooks.

Sink rules let you check more library functions without waiting for an SDK release. For example, you can treat a database driver’s raw query method as a SQL sink. A rule is data, not code. It names a function and one of the checks above, and the SDK runs that check on the call. Rules reach the SDK in the signed config, so they’re trusted exactly as far as your pinned key.

Add an app’s rules under Sink rules in its Runtime protection settings, as a JSON list:

[
{
"id": "sink.example.query",
"module": "example-driver",
"target": "Connection.prototype.query",
"check": "sql",
"dialect": "postgres",
"arg": 0,
"field": "sql",
"action": "monitor"
}
]
  • module is the package as your app loads it, and target is the path from its exports to the function, at most 4 segments.
  • check is one of sql, nosql, command, path, ssrf, code, deser (with format) or xml (with resolvesExternal).
  • arg is the argument to check, and field names a property when that argument is an object.
  • action defaults to monitor. A rule blocks only when it says block and its attack type is set to block.
  • Up to 64 rules per app, including the built-in ones. Shield360 refuses an invalid rule and names it.
  • Node and Python apply sink rules. Go and .NET ignore them, because their code is compiled and can’t be patched at runtime.

Events from a rule show the sink rule:<id>. The full format is in “Sink rules” in sdk/rasp/README.md.

The SDK reports each attack to POST /api/guard/events with the app’s API key. It records the attack type, rule, sink, route template, HTTP method, decision, source (request or model_output) and the input’s location, such as query.id, never its value. Repeats within 10 seconds are folded into one event with a count. Shield360 checks every field and drops an event that fails a check. How long events are kept is your choice (see Retention).

The app’s Runtime protection section shows:

  • attempts, blocked and monitored, and the rules hit,
  • attempts by attack type, and the routes attacked most,
  • the latest events,
  • vulnerable components: the packages the app reports (names and versions only, at start and daily) that appear in Shield360’s bundled advisory list. The list is short and covers packages that runtime protection instruments. A package that is not on it is unknown, not safe.

How long runtime protection data is kept is your organisation’s choice, under Settings → Data retention → Runtime protection data:

DataDefaultRange
Exploit attempts400 days1 to 3650 days
Reported packages90 days1 to 3650 days
Matched-input snippets30 days1 to 30 days, and never longer than the attempts

Each environment can keep them for less than the organisation. Every row gets its expiry when it is stored. If you shorten a period, Shield360 asks you to confirm, then removes that environment’s older rows (snippets are emptied) and brings the expiry of the remaining rows forward. The change is recorded in the audit log.

  • Each attack raises a rasp.attack alert, one per app, attack type, rule and decision within 15 minutes. A blocked attack is a warning. A monitored attack is critical, because the call went through. The alert carries the app, attack type, rule, sink, route, decision and CVE. It never carries the input.
  • The detection signal Runtime protection events (rasp_events) can be grouped by app, attack type, sink and decision. Two templates are ready to turn on: Blocked exploit attempt and Exploit attempts spiking (compared with the app’s own two-week norm).
  • Vulnerabilities in your own business logic, such as broken access control or a missing authorization check. Runtime protection sees how input reaches a sink, not whether the caller should be allowed to make the call.
  • Attacks that never reach an instrumented sink, or sinks your SDK does not hook (native extensions, and drivers or parsers outside the list above).
  • Input that does not come from a request or a model, such as a file another process wrote. These are checked only by the allow-lists and the virtual patches.
  • Stored XSS, and blocking XSS: reflected XSS is monitored only.
  • Denial of service and volumetric attacks.
  • PHP. Support is planned.

Runtime protection works alongside a web application firewall, dependency upgrades and code review. It does not replace them.

Several detection algorithms are adapted from Baidu OpenRASP’s official plugin (plugins/official/plugin.js, Copyright 2017-2021 Baidu Inc., Apache License 2.0). They include the SQL user-input token algorithm and its keyword list, the command user-input check, the path checks, the script-file write and NTFS stream checks, the SSRF metadata, out-of-band domain and protocol lists, the XXE protocol and file checks, the reflected-XSS tag check, and the extra input sources. Each SDK re-implements this logic in its own language. No OpenRASP code is copied. Attribution is in the repository’s NOTICE.