The Class Path Nobody Denied — Java Bug Hunt
Modelled on Spring4Shell (CVE-2022-22965, March 2022): Spring MVC's data binding let request parameters walk a property path from the form object into…
- Language: Java
- Layer: Backend
- Difficulty: Hard
- Concepts: Security, Validation
- Modelled on: Spring · CVE-2022-22965
- Visible tests: a plain property binds; the route through the module property is refused; the old direct route stays refused
- Reward: 50 XP for a complete fix
Briefing
Modelled on Spring4Shell (CVE-2022-22965, March 2022): Spring MVC's data binding let request parameters walk a property path from the form object into class.module.classLoader…. An older fix had blocked the path class.classLoader, but on JDK 9+ the class loader was also reachable through the new module property, and on some Tomcat deployments that was enough for remote code execution.
This project is a reconstruction without reflection: the form object is a tree of maps, and every object exposes a class entry just as every Java bean exposes getClass(); the JDK 9 route is the jdkModule entry and the class loader is loader. DataBinder.bind walks the dotted path and sets the leaf — guarded by a denylist of one exact prefix.
Fix bind so no property path can ever travel through class.
Bug report
BUG-S4S · Priority: Critical · Reported by: security
DataBinder.bind(target, path, value) walks a dotted path through nested maps and sets the leaf, returning true when it bound and false when it refused.
- a path is refused (false, nothing changed) if ANY of its segments is "class", compared case-insensitively — wherever it appears in the path
- a path is refused if an intermediate segment is missing or is not a map
- a path is refused if the leaf does not already exist or is itself a map (binding only sets existing simple properties)
- otherwise the leaf is set to value and bind returns true
Observed: class.loader.* is refused, but class.jdkModule.loader.logConfig.pattern binds and rewrites the container's logging pattern.
Logs
[binder] refused class.loader.logConfig.pattern
[binder] bound class.jdkModule.loader.logConfig.pattern = "<attacker text>"The code as shipped
src/web/DataBinder.java (editable)
class DataBinder {
@SuppressWarnings("unchecked")
static boolean bind(Map<String, Object> target, String path, String value) {
if (path.toLowerCase().startsWith("class.loader")) return false;
String[] segs = path.split("\\.");
Map<String, Object> node = target;
for (int i = 0; i < segs.length - 1; i++) {
Object next = node.get(segs[i]);
if (!(next instanceof Map)) return false;
node = (Map<String, Object>) next;
}
String leaf = segs[segs.length - 1];
if (!node.containsKey(leaf) || node.get(leaf) instanceof Map) return false;
node.put(leaf, value);
return true;
}
}
Read-only context: src/web/FormObject.java.
Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Java bug hunts.