Use of Externally-Controlled Input to Select Classes or Code
Description
Use of Externally-Controlled Input to Select Classes or Code occurs when software uses external input to determine which classes to instantiate, which code paths to execute, or which methods to invoke. This is commonly seen in object deserialization, reflection-based instantiation, dynamic class loading, and plugin systems. Attackers can manipulate this input to load malicious classes, execute arbitrary code, or access unintended functionality.
Risk
This vulnerability enables some of the most severe attacks in software security. Insecure deserialization has been consistently ranked in the OWASP Top 10 and has caused numerous high-profile breaches. Attackers can achieve remote code execution (RCE) by specifying malicious class names that execute code during instantiation or deserialization. Java's deserialization vulnerabilities (using gadget chains like in Apache Commons Collections) have compromised countless enterprise systems. Similar issues exist in PHP's unserialize(), Python's pickle, and .NET's BinaryFormatter.
Solution
Avoid using external input to select classes whenever possible. Implement strict allowlists of permitted class names if dynamic loading is required. Never deserialize untrusted data with native serialization mechanisms—use safe alternatives like JSON. Validate and sanitize all input used in reflection or class loading. Use type-safe deserialization libraries. Implement look-ahead deserialization that validates class names before instantiation. Remove dangerous gadget classes from the classpath. Consider using signed serialized data.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Remote Code Execution Attackers can execute arbitrary code on the server through malicious class instantiation. |
| Integrity | Scope: System Compromise Full system takeover through code execution during deserialization. |
| Availability | Scope: Denial of Service Resource exhaustion through malicious object construction. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Dynamic class loading from user input
class VulnerableFactory:
def create_handler(self, handler_type):
# User controls which class is instantiated!
module = __import__('handlers')
handler_class = getattr(module, handler_type)
return handler_class()
# Attacker input: "os.system('rm -rf /')"
# VULNERABLE: Pickle deserialization
import pickle
import base64
def load_session_vulnerable(session_data):
# Deserializing untrusted data!
return pickle.loads(base64.b64decode(session_data))
# Attacker payload:
class Exploit:
def __reduce__(self):
import os
return (os.system, ('id',))
# VULNERABLE: exec/eval with user input
def execute_action_vulnerable(action_code):
# Direct code execution!
exec(action_code)
# VULNERABLE: Dynamic import
def load_plugin_vulnerable(plugin_name):
# User controls import path!
plugin = __import__(plugin_name)
return plugin.run()
# VULNERABLE: YAML unsafe load
import yaml
def load_config_vulnerable(yaml_string):
# Unsafe YAML loading allows arbitrary Python objects!
return yaml.load(yaml_string) # No Loader specified!
# Attacker YAML:
# !!python/object/apply:os.system ['id']
// VULNERABLE: Java deserialization
public class VulnerableDeserialization {
public Object deserialize(byte[] data) throws Exception {
// Deserializing untrusted data!
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject(); // RCE if gadget chains present!
}
}
// VULNERABLE: Dynamic class instantiation
public class VulnerableFactory {
public Object createObject(String className) throws Exception {
// User controls class name!
Class<?> clazz = Class.forName(className);
return clazz.getDeclaredConstructor().newInstance();
}
public void invokeMethod(String className, String methodName, Object[] args)
throws Exception {
// User controls both class and method!
Class<?> clazz = Class.forName(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod(methodName, getParameterTypes(args));
method.invoke(instance, args);
}
}
// VULNERABLE: XMLDecoder
import java.beans.XMLDecoder;
public class VulnerableXMLDecoder {
public Object decode(InputStream is) {
// XMLDecoder can instantiate arbitrary objects!
XMLDecoder decoder = new XMLDecoder(is);
return decoder.readObject();
}
}
// VULNERABLE: Spring class name in request
@RestController
public class VulnerableController {
@PostMapping("/execute")
public Object execute(@RequestParam String className) throws Exception {
// Class from request parameter!
Class<?> clazz = Class.forName(className);
return clazz.getDeclaredConstructor().newInstance();
}
}
// VULNERABLE: eval with user input
app.post('/execute', (req, res) => {
const code = req.body.code;
// Direct eval of user input!
const result = eval(code);
res.json({ result });
});
// VULNERABLE: new Function()
app.post('/calculate', (req, res) => {
const formula = req.body.formula;
// User controls function body!
const fn = new Function('return ' + formula);
res.json({ result: fn() });
});
// VULNERABLE: Dynamic require
app.get('/plugin/:name', (req, res) => {
const pluginName = req.params.name;
// User controls require path!
const plugin = require('./plugins/' + pluginName);
res.json(plugin.run());
});
// VULNERABLE: Node.js unserialize
const serialize = require('node-serialize');
app.post('/session', (req, res) => {
const sessionData = req.body.session;
// Deserializing untrusted data!
const session = serialize.unserialize(sessionData);
res.json(session);
});
// Attacker payload:
// {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('id')}()"}
// VULNERABLE: vm.runInContext
const vm = require('vm');
app.post('/eval', (req, res) => {
const code = req.body.code;
const context = vm.createContext({});
// User code execution in VM (can be escaped!)
const result = vm.runInContext(code, context);
res.json({ result });
});
Fixed Code
# SAFE: Allowlist of permitted classes
class SecureFactory:
ALLOWED_HANDLERS = {
'json': 'handlers.JsonHandler',
'xml': 'handlers.XmlHandler',
'csv': 'handlers.CsvHandler'
}
def create_handler(self, handler_type):
if handler_type not in self.ALLOWED_HANDLERS:
raise ValueError(f"Invalid handler type: {handler_type}")
module_path, class_name = self.ALLOWED_HANDLERS[handler_type].rsplit('.', 1)
module = __import__(module_path, fromlist=[class_name])
handler_class = getattr(module, class_name)
return handler_class()
# SAFE: Use JSON instead of pickle
import json
def load_session_safe(session_data):
"""Use JSON for safe deserialization."""
return json.loads(base64.b64decode(session_data))
# SAFE: If pickle is required, use restricted unpickler
import pickle
import io
class RestrictedUnpickler(pickle.Unpickler):
ALLOWED_CLASSES = {
('myapp.models', 'User'),
('myapp.models', 'Session'),
}
def find_class(self, module, name):
if (module, name) not in self.ALLOWED_CLASSES:
raise pickle.UnpicklingError(
f"Forbidden class: {module}.{name}"
)
return super().find_class(module, name)
def restricted_loads(data):
"""Safely unpickle with class restrictions."""
return RestrictedUnpickler(io.BytesIO(data)).load()
# SAFE: No dynamic code execution
def execute_action_safe(action_name, params):
"""Use action mapping instead of exec."""
ACTIONS = {
'send_email': send_email_action,
'generate_report': generate_report_action,
'notify_user': notify_user_action,
}
if action_name not in ACTIONS:
raise ValueError(f"Unknown action: {action_name}")
return ACTIONS[action_name](**params)
# SAFE: Plugin loading with allowlist
ALLOWED_PLUGINS = {'analytics', 'reporting', 'export'}
def load_plugin_safe(plugin_name):
if plugin_name not in ALLOWED_PLUGINS:
raise ValueError(f"Plugin not allowed: {plugin_name}")
# Sanitize to prevent path traversal
safe_name = plugin_name.replace('.', '').replace('/', '').replace('\\', '')
plugin = __import__(f'plugins.{safe_name}', fromlist=['run'])
return plugin.run()
# SAFE: YAML safe loading
import yaml
def load_config_safe(yaml_string):
"""Use safe_load to prevent arbitrary object instantiation."""
return yaml.safe_load(yaml_string)
// SAFE: Java with deserialization filter
public class SecureDeserialization {
private static final Set<String> ALLOWED_CLASSES = Set.of(
"com.myapp.model.User",
"com.myapp.model.Session",
"java.util.ArrayList",
"java.util.HashMap"
);
public Object deserialize(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String className = desc.getName();
if (!ALLOWED_CLASSES.contains(className)) {
throw new InvalidClassException(
"Unauthorized deserialization attempt",
className
);
}
return super.resolveClass(desc);
}
};
return ois.readObject();
}
// Using Java 9+ ObjectInputFilter
public Object deserializeWithFilter(byte[] data) throws Exception {
ByteArrayInputStream bis = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bis);
ois.setObjectInputFilter(filterInfo -> {
Class<?> clazz = filterInfo.serialClass();
if (clazz == null) {
return ObjectInputFilter.Status.ALLOWED;
}
String className = clazz.getName();
if (ALLOWED_CLASSES.contains(className) ||
className.startsWith("java.lang.") ||
className.startsWith("[")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
return ois.readObject();
}
}
// SAFE: Secure factory with allowlist
public class SecureFactory {
private static final Map<String, Class<?>> ALLOWED_CLASSES = Map.of(
"json", JsonHandler.class,
"xml", XmlHandler.class,
"csv", CsvHandler.class
);
public Handler createHandler(String type) {
Class<?> clazz = ALLOWED_CLASSES.get(type);
if (clazz == null) {
throw new IllegalArgumentException("Invalid handler type: " + type);
}
try {
return (Handler) clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to create handler", e);
}
}
}
// SAFE: Use JSON instead of Java serialization
import com.fasterxml.jackson.databind.ObjectMapper;
public class SafeSerialization {
private final ObjectMapper mapper = new ObjectMapper();
public String serialize(Object obj) throws Exception {
return mapper.writeValueAsString(obj);
}
public <T> T deserialize(String json, Class<T> type) throws Exception {
return mapper.readValue(json, type);
}
}
// SAFE: Spring with validated enum
@RestController
public class SecureController {
public enum HandlerType {
JSON, XML, CSV
}
@PostMapping("/execute")
public ResponseEntity<?> execute(@RequestParam HandlerType type) {
// Type is validated by Spring - only enum values accepted
Handler handler = HandlerFactory.create(type);
return ResponseEntity.ok(handler.process());
}
}
// SAFE: Allowlist instead of eval
const OPERATIONS = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => b !== 0 ? a / b : null
};
app.post('/calculate', (req, res) => {
const { operation, a, b } = req.body;
if (!OPERATIONS[operation]) {
return res.status(400).json({ error: 'Invalid operation' });
}
const result = OPERATIONS[operation](Number(a), Number(b));
res.json({ result });
});
// SAFE: Plugin loading with allowlist
const ALLOWED_PLUGINS = new Set(['analytics', 'reporting', 'export']);
app.get('/plugin/:name', (req, res) => {
const pluginName = req.params.name;
if (!ALLOWED_PLUGINS.has(pluginName)) {
return res.status(400).json({ error: 'Invalid plugin' });
}
// Safe because name is from allowlist
const plugin = require(`./plugins/${pluginName}`);
res.json(plugin.run());
});
// SAFE: JSON instead of serialize
app.post('/session', (req, res) => {
try {
// Use JSON - no code execution
const session = JSON.parse(req.body.session);
// Validate structure
if (!session.userId || typeof session.userId !== 'string') {
throw new Error('Invalid session format');
}
res.json(session);
} catch (e) {
res.status(400).json({ error: 'Invalid session data' });
}
});
// SAFE: Expression evaluation with math.js (sandboxed)
const math = require('mathjs');
// Create limited math instance
const limitedMath = math.create(math.all);
limitedMath.import({
import: function () { throw new Error('disabled'); },
createUnit: function () { throw new Error('disabled'); },
evaluate: function () { throw new Error('disabled'); },
parse: function () { throw new Error('disabled'); },
simplify: function () { throw new Error('disabled'); },
derivative: function () { throw new Error('disabled'); }
}, { override: true });
app.post('/math', (req, res) => {
try {
const expression = req.body.expression;
// Only allow basic math operations
const result = limitedMath.evaluate(expression);
res.json({ result });
} catch (e) {
res.status(400).json({ error: 'Invalid expression' });
}
});
// SAFE: Factory pattern with registry
class HandlerRegistry {
constructor() {
this.handlers = new Map();
}
register(name, HandlerClass) {
this.handlers.set(name, HandlerClass);
}
create(name, ...args) {
const HandlerClass = this.handlers.get(name);
if (!HandlerClass) {
throw new Error(`Unknown handler: ${name}`);
}
return new HandlerClass(...args);
}
}
const registry = new HandlerRegistry();
registry.register('json', JsonHandler);
registry.register('xml', XmlHandler);
registry.register('csv', CsvHandler);
app.post('/process', (req, res) => {
try {
const handler = registry.create(req.body.type);
res.json(handler.process(req.body.data));
} catch (e) {
res.status(400).json({ error: e.message });
}
});
Exploited in the Wild
Apache Struts (Equifax Breach - 2017)
CVE-2017-5638 allowed remote code execution through OGNL expression injection, affecting 147 million people.
Apache Commons Collections
Gadget chains in Commons Collections enabled RCE through Java deserialization on thousands of systems.
Jenkins, WebLogic, JBoss
Multiple critical RCE vulnerabilities through Java deserialization have affected major enterprise platforms.
Tools to test/exploit
-
ysoserial — Java deserialization payload generator.
-
marshalsec — Java unmarshaller exploits.
-
PHPGGC — PHP gadget chain generator.
-
Burp Suite — test deserialization endpoints.
CVE Examples
-
CVE-2017-5638 — Apache Struts RCE.
-
CVE-2015-7501 — Apache Commons Collections.
-
CVE-2020-9484 — Apache Tomcat deserialization.
References
-
MITRE. "CWE-470: Use of Externally-Controlled Input to Select Classes or Code." https://cwe.mitre.org/data/definitions/470.html
-
OWASP. "Insecure Deserialization." https://owasp.org/Top10/A08_2021-Software_and_Data_Integrity_Failures/