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.