Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building a Runtime Context Capture System

Hello! Welcome back to our course on architecting security products.

In our last lesson, we established the foundational "how" of Runtime Application Self-Protection (RASP) by architecting three distinct instrumentation approaches: bytecode modification, language-level hooks, and native library interposition. We now understand how a RASP can place its sensors inside a running application.

Today, we move from placing the sensors to designing what they do. Our goal is to answer the question: once a hook is triggered, what data does it collect, and how do we design the system that gathers and manages this information? This brings us to the core of RASP's intelligence: its context-awareness.

The learning outcome for this lesson is to: Design a runtime context capture system to collect data like HTTP parameters and SQL queries via instrumented hooks.

We will architect the data pipeline that allows a RASP to connect the dots between user input and risky operations, a capability that sets it far apart from network-level security tools.


1. The Power of Runtime Context

Let's start with a quick recap. A Web Application Firewall (WAF) sees a request like POST /login with a payload username=' OR 1=1;--. It must decide if this is malicious based on traffic patterns alone. A RASP, however, has a much richer view. It can see not only the incoming request but also what the application does with it. Does the payload ' OR 1=1;-- end up being concatenated into a SQL query string? That's the critical piece of information—the runtime context—that enables high-fidelity detection.

Our task is to design a system that captures this context. This system needs to answer three questions:

  1. Source: Where did the potentially malicious data originate? (e.g., an HTTP request parameter)
  2. Sink: Where is that data being used in a potentially dangerous way? (e.g., in a SQL query)
  3. Execution Flow: How did the data get from the source to the sink? (e.g., the application code path, represented by a stack trace)

An effective RASP context capture system gathers all three.

Java Bytecode Instrumentation with JVM Agent
As a refresher from our last lesson, this diagram shows how a JVM agent uses bytecode instrumentation to modify a class as it's loaded. We will now design the logic that this injected code executes to capture runtime context.

2. Architecting the Context Capture System

Let's use a common scenario to guide our design: a Java-based web application receiving an HTTP request that leads to a database query. Our context capture system needs to link these two events, even though they happen at different times and in different parts of the code.

Given your background, you'll recognize this as a challenge of state management in a concurrent environment. A web server handles numerous requests simultaneously on different threads. The context for Request A on Thread 1 must remain completely isolated from Request B on Thread 2.

The classic design pattern to solve this is using Thread-Local Storage. A thread-local variable provides each thread with its own, independently initialized copy of the variable. One thread cannot access or modify another thread's copy. This is perfect for storing our request-specific context.

ThreadLocal Variable Isolation in Multi-threaded Environments
This diagram illustrates how ThreadLocal variables work. Each thread (`Thread 1`, `Thread 2`) gets its own `ThreadLocalMap`, ensuring that the values associated with a ThreadLocal object are isolated per thread. This is the mechanism we will use to store the runtime context for each individual request.

Our architecture will therefore be built around a RequestContext object stored in a ThreadLocal variable. Let's outline the data flow.

Step 1: Hooking the Entry Point and Capturing the Source

First, we need to capture the source of the input. For a web application, the ultimate source is the incoming HTTP request. We'll instrument the application's main entry point. In the Java Servlet world, a perfect candidate is the service method of javax.servlet.Servlet or javax.servlet.http.HttpServlet.

Our hook, injected via bytecode instrumentation, will fire at the beginning of this method. Its job is to:

  1. Create a new RequestContext object.
  2. Populate it with details from the HttpServletRequest object, which is available as a method parameter. This includes the URI, headers, method, query parameters, and source IP.
  3. Store this RequestContext object in a ThreadLocal variable.
// Conceptual RASP Agent Code
public class RequestContext {
    String uri;
    String method;
    Map<String, String> headers;
    Map<String, String[]> parameters;
    String remoteAddr;
    List<SqlCall> sqlCalls;
    // ... other fields
}

public class RASPContextManager {
    public static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
}

// Injected into the start of HttpServlet.service(req, resp)
try {
    RequestContext ctx = new RequestContext();
    ctx.setUri(req.getRequestURI());
    ctx.setParameters(req.getParameterMap());
    // ... populate other fields
    RASPContextManager.CONTEXT.set(ctx);

    // ... original method body runs here ...

} finally {
    // This is crucial! Always clean up to prevent memory leaks in thread pools.
    RASPContextManager.CONTEXT.remove();
}

The try...finally block is a critical design element. It guarantees that the context for a request is cleared after the request has been processed, preventing memory leaks and ensuring that a reused thread doesn't carry stale context into a new request.

Step 2: Hooking the Sink and Tying it to the Source

Next, we instrument the sinks—the dangerous functions where user input might be used. A prime example is database query execution. In Java, this means hooking methods like java.sql.Statement.execute(String sql).

When our hook on Statement.execute() fires, it performs these steps:

  1. Retrieve the current RequestContext from the ThreadLocal variable: RASPContextManager.CONTEXT.get().
  2. If no context exists, it means this database call didn't originate from a web request we're monitoring, so we can ignore it.
  3. If context does exist, we capture the details of the sink call. The SQL query string is available as a method argument.
  4. We also capture the execution context by generating a stack trace. This tells us which part of the application code is making the database call.
  5. Add this new information (the SQL call and stack trace) to the RequestContext object.

At this moment, the RASP agent has the complete picture: it knows the HTTP request (source), the SQL query (sink), and the code path that connected them (execution context). This rich, combined context can now be passed to a detection engine for analysis.

To see how this is implemented in detail, let's turn to a real-world example from a research paper on a RASP tool named "Griffin".

[PDF] specification-based RASP approach against SQL injections in MySQL

The paper 'specification-based RASP approach against SQL injections in MySQL' describes a tool called Griffin. It details exactly how it captures the context around SQL queries. Please read the following sections to understand their design.

Please read the following sections: Section 4.2 Learning phase (page 21-22): Focus on how it collects the "method and class name that issued the query" from the stack trace and creates an abstract structure of the query. This is a great example of capturing both execution and sink context. Section 4.5.1 Specifications intended for the queries (page 27-28): Pay close attention to the description of how the class and method name are extracted from the stack trace and how the query structure is built by identifying and replacing values. This is a concrete implementation of a context capture algorithm.

The Griffin paper highlights a key design choice: capturing the application's method and class name from the stack trace. This is crucial for pinpointing the exact location of a vulnerability in the application's own source code, which is invaluable information for developers.

Step 3: Handling Complex Sinks (e.g., Prepared Statements)

Our design works well for simple Statement.execute(sql) calls. But modern applications almost always use PreparedStatement, where the query structure and parameters are sent separately.

  1. Connection.prepareStatement("SELECT * FROM users WHERE name = ?")
  2. PreparedStatement.setString(1, "user_input")
  3. PreparedStatement.execute()

This breaks our simple model. The execute() call has no arguments, and the SQL string is only available when the statement was first created. We need a more sophisticated context capture design that involves multiple, coordinated hooks.

Here's an improved architecture:

  • Hook Connection.prepareStatement(sql): When this is called, retrieve the RequestContext from ThreadLocal and store the SQL template (...WHERE name = ?) in it.
  • Hook PreparedStatement.setXXX(index, value) methods: When these are called, retrieve the context and add the parameter (index, value) to a list of parameters within the RequestContext object.
  • Hook PreparedStatement.execute(): When this fires, the RequestContext object in the ThreadLocal already contains the full picture: the SQL template and all the parameters that were bound to it.

This design correctly reconstructs the full query and ties it to the originating HTTP request. The Griffin paper you just read also discusses this challenge and its solution, demonstrating its importance in a real-world RASP design.

The actual mechanism for extracting method arguments relies on the bytecode instrumentation libraries we discussed in the last lesson.

The definitive guide to Java agents by Rafael Winterhalter

Let's revisit 'The definitive guide to Java agents' by Rafael Winterhalter. He demonstrates exactly how a library like ByteBuddy can capture method arguments using annotations, which is the mechanism we'd use for our setXXX hooks.

Please re-watch the segment on ByteBuddy's Advice templates and annotations (14:45 - 23:27). Focus specifically on the part where he introduces annotations like @Advice.Argument(0) (around 21:08). This shows how you can programmatically access the arguments of an instrumented method (like the index and value from setString(index, value)) from within your RASP agent's code.

3. A Polyglot Perspective

While our example focused on Java, the architectural principles of a context capture system are universal. The implementation details, however, are language-specific.

The LL-RASP paper from our previous lesson provides excellent examples of how to capture one crucial piece of context—the stack trace—across different languages at the native level.

[PDF] Low-level RASP: Protecting Applications Implemented in ... - Black Hat

The presentation 'Low-level RASP' shows concrete code for getting a stack trace in multiple languages. This illustrates how the fundamental concept of 'capturing execution context' is adapted to different runtimes.

Quickly review the slides under 'Implementation' for Java, Node, Python, and Ruby (slides 14-18). You don't need to understand the C++ code in detail, but observe that for each language, there is a specific function (dump_stack_trace) that uses the runtime's native API (JNI for Java, V8 API for Node.js, etc.) to generate the call stack. This reinforces the idea of a common architectural goal with platform-specific implementations.

This shows that whether you're building a RASP for Java, Python, or Node.js, the design of your context capture system will follow the same pattern: hook entry points and sinks, and use a request-scoped storage mechanism (like Thread-Local storage or its equivalent) to correlate the events.


Conclusion

In this lesson, we designed the heart of a RASP's intelligence: its runtime context capture system. We've moved beyond simply placing hooks to architecting a coherent system that gives those hooks meaning.

Key Takeaways:

  • Runtime context is a combination of Source, Sink, and Execution Flow information. It's what allows a RASP to understand the story of a request from start to finish.
  • Thread-Local Storage is a fundamental design pattern for managing request-scoped context in concurrent applications, preventing data from different requests from interfering with each other.
  • Capturing context for complex operations like Prepared Statements requires a stateful design with multiple, coordinated hooks to reconstruct the full event.
  • The captured context is the structured data that will fuel the detection engine. Our system is a data collection pipeline purpose-built for security analysis.

Preview of the Next Lesson

We have now designed a system that can see what an application is doing in great detail. We can connect an incoming HTTP parameter to the exact SQL query it's used in. The next logical question is: how do we decide if that action is malicious?

In our next lesson, we will build the brain of our RASP. The learning outcome will be: Design a vulnerability detection engine that analyzes captured runtime context for threats like SQL injection and command injection. We'll explore different analysis techniques, from simple pattern matching to more advanced semantic analysis, that operate on the rich context we've just learned how to collect.

Can't find a good explanation? Sign up and we'll make it for you

Sign up