Runtime protection (RASP): stop exploits inside your app
Overview
Section titled “Overview”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.
What it protects, per SDK
Section titled “What it protects, per SDK”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 type | Checked at | Examples |
|---|---|---|
sql_injection | SQL drivers (dialect from the driver) | pg.query and the drivers each SDK wraps |
nosql_injection | MongoDB filters and updates | operator objects that came from the request body or query |
command_injection, shell_spawn | Shell commands and process starts | Node exec/spawn with a shell, Python os.system/subprocess(shell=True), Go rasp.Command, .NET cmd /c and sh -c |
path_traversal, file_write | File reads, writes, deletes, renames and directory listings | fs.readFile, Python open |
ssrf, outbound | Outbound HTTP, on the address the connection actually uses | Node socket lookup, Python getaddrinfo, Go DialContext, .NET SocketsHttpHandler.ConnectCallback |
deserialization | Unsafe deserializers | pickle and marshal, PyYAML and js-yaml unsafe loaders, jsonpickle, node-serialize, funcster, .NET BinaryFormatter and TypeNameHandling |
code_injection | Code evaluation | eval, new Function, vm, Python eval/exec/compile |
xxe | XML parsers that resolve external entities | libxmljs, xml2js, lxml, XmlReaderSettings. Python xml.etree and Go encoding/xml report xxe.doctype only |
xss | HTML responses (monitor only) | reflected input in an HTML body |
virtual_patch | Each request, before your handler runs | the 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.
Turn it on
Section titled “Turn it on”Runtime protection is off until the app enables it:
| SDK | Enable |
|---|---|
| TypeScript | rasp: true |
| Python | rasp=True |
| Go | rasp.Enable(...) |
| .NET | AddShield360Rasp() |
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).
Kill switch
Section titled “Kill switch”Any of these stops every check at once. They are listed in order of precedence:
- The environment variable
SHIELD360_RASP=offon the app. - The Kill switch on the app’s Runtime protection settings in Shield360. Protection on set to off has the same effect.
- 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 decisionblocked. - 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 ForbiddenContent-Type: application/json
{"error":"Request blocked by Shield360 runtime protection","attack":"sql_injection","rule":"sqli.userinput","event":"<event id>"}Settings in Shield360
Section titled “Settings in Shield360”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.
| Setting | Default | Meaning |
|---|---|---|
| Protection on | on | Off stops the app’s checks, the same as the kill switch. |
| Kill switch | off | Emergency stop. Every check returns at once, whatever else is set. |
| Default mode | monitor | The mode for every attack type that has no mode of its own. |
| Mode per attack type | default | off, monitor or block. XSS offers monitor only. |
| Allowed commands | none | Executable names the app may start during requests without a shell_spawn event. |
| Outbound allow-list | none | Host 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 roots | none | Absolute directories. A file path that holds input and lies outside every root is path_traversal. |
| Send matched-input snippets | off | Sends 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.
Pin the config key
Section titled “Pin the config key”The signed config can turn protection off and widen allow-lists, so the SDK trusts it only under a key you give it yourself:
- 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).
- Give it to the SDK as
configPublicKey(TypeScript),config_public_key(Python),ConfigPublicKey(Go, .NET), or the environment variableSHIELD360_CONFIG_PUBLIC_KEY.
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
allowUnpinnedConfigorSHIELD360_CONFIG_TOFU=dev, and the SDK ignores it when the app runs in production. - The
keysfield of the config response is informational. The SDK never trusts it in place of a pin.
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.
Virtual patches
Section titled “Virtual patches”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:
| Rule | Protects against | CVE |
|---|---|---|
vp.react2shell | React Server Components Flight deserialization RCE (React2Shell), through a Next.js server action | CVE-2025-55182, CVE-2025-66478 |
vp.react2shell-flight | The same flaw, without a Next-Action header | CVE-2025-55182 |
vp.next-middleware-bypass | Next.js middleware authorization bypass with x-middleware-subrequest | CVE-2025-29927 |
vp.node-serialize-iife | node-serialize unserialize() code execution | CVE-2017-5941 |
vp.node2shell-brotli | Node.js Brotli decoder out-of-bounds write (Node2Shell). It refuses Brotli-encoded request bodies on unpatched Node.js 22, 24 and 26 | CVE-2026-111497 |
vp.example-test-header | Not a real advisory. It matches the header x-shield360-rasp-test: exploit, so you can test the pipeline from end to end | n/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.
Security baseline
Section titled “Security baseline”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.
| Rule | Means |
|---|---|
baseline.tls_verify_off | Certificate checks are off for every outbound connection. |
baseline.inspector_in_production | A debugger is listening, or was requested, in production. |
baseline.development_mode_in_production | The app runs in development mode while its environment says production. |
baseline.running_as_root | The process runs as root or an elevated administrator. |
baseline.config_key_unpinned | In production, the signed config key isn’t pinned. |
baseline.runtime_end_of_life | The 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
Section titled “Sink rules”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" }]moduleis the package as your app loads it, andtargetis the path from its exports to the function, at most 4 segments.checkis one ofsql,nosql,command,path,ssrf,code,deser(withformat) orxml(withresolvesExternal).argis the argument to check, andfieldnames a property when that argument is an object.actiondefaults tomonitor. A rule blocks only when it saysblockand 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.
Events and the Security tab
Section titled “Events and the Security tab”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.
Retention
Section titled “Retention”How long runtime protection data is kept is your organisation’s choice, under Settings → Data retention → Runtime protection data:
| Data | Default | Range |
|---|---|---|
| Exploit attempts | 400 days | 1 to 3650 days |
| Reported packages | 90 days | 1 to 3650 days |
| Matched-input snippets | 30 days | 1 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.
Alerts and detections
Section titled “Alerts and detections”- Each attack raises a
rasp.attackalert, 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).
What it does not cover
Section titled “What it does not cover”- 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.
Provenance
Section titled “Provenance”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.