How this bug class keeps getting shipped

The lead item in CISA's Known Exploited Vulnerabilities catalog on 31 August 2026 was a chain against PaperCut NG/MF: CVE-2026-81578 (missing authentication on a configuration endpoint) plus CVE-2026-82078 (unsafe reflection that loads arbitrary Java bytecode already on the classpath). Read together, they express a single bug class in two halves: an unauthenticated write into server state, and a sink that treats server state as a pointer to code. The catalog additions from the preceding week reinforce the pattern. AjaxPro's deserialization flaw (CVE-2021-23758) lets a request name a .NET class to instantiate. Gitea's diffpatch endpoint (CVE-2026-60004) accepts a patch that plants an executable Git hook, and the next repository operation runs it. Different vendors, different runtimes, same underlying mistake: the code path that decides which code to run is downstream of an input an attacker controls.

This post dissects that class: what the sinks look like in source, why they keep passing review, and what controls actually close the failure mode.

Anatomy of attacker-controlled dispatch

Somewhere in the request-handling stack, a value from an HTTP parameter, a serialized payload, a configuration entry, or a file path selects the next instruction: a class to load, a method to invoke, a callback to register, a script to source, a hook to run. The dispatch is legitimate; frameworks depend on it. The failure is scope. When the set of dispatchable targets is anything the runtime can find and the selector is untrusted, the sink becomes a remote-code-execution primitive dressed as configuration.

Three variants dominate the CVE record.

  1. Reflective invocation. The server reads a class or method name, resolves it through Class.forName, getDeclaredMethod, MethodHandles.Lookup, or the equivalent in .NET or Python, and calls it. CVE-2026-82078 sits here. The advisory language ("execute arbitrary Java bytecode residing on the application classpath") is the giveaway: no upload was required; the gadget chains shipped with the product.
  1. Deserialization. A binary or XML blob passes to a formatter that reconstructs object graphs, triggering constructors, property setters, or callbacks. CVE-2021-23758 in AjaxPro is a textbook case; the sink instantiates arbitrary .NET types from the request. The application never chose to run those types; the formatter did, by design.
  1. Hook or handler planting. The attacker writes a file at a path the runtime later executes as code. CVE-2026-60004 in Gitea uses the diffpatch API to introduce a Git hook that fires on the next repository event. The exploit primitive is deferred: code execution through a well-known filename.

Remediation differs by variant. Reflective sinks need allow-lists; deserialization needs a type-safe format; hook planting needs a write barrier around any path the runtime treats as executable.

Why it survives review

Three patterns recur.

The "harmless setter." An endpoint appears to change a string, a number, or a URL. The reviewer sees settings.put(key, value) and moves on. What they miss is that some key values later feed a reflective loader, a URL fetcher, or a template compiler. CVE-2026-81578 is exactly this: an unauthenticated configuration write that is not, on its own, RCE. It becomes RCE only when combined with the reflective sink in CVE-2026-82078. Either half reviewed in isolation reads as low severity.

Trust of internal callers. Reflective dispatch and deserialization typically enter the codebase for internal RPC or plugin systems. Once present, the code migrates to endpoints reachable by external requests. The AjaxPro pattern (an AJAX layer that lets the browser name the handler class) shipped when the intranet was still treated as a trust boundary. The code outlived the assumption.

Filesystem semantics. Path-restriction bugs like CVE-2026-66384 in JFrog Artifactory (writes outside the intended Docker cache path under specific remote-repository conditions) read as data-integrity issues until you notice that "outside the intended path" can mean a directory the runtime treats as code, configuration, or a hook. The Gitea diffpatch case teaches the same lesson: any write primitive against a runtime-observed path is a code-execution primitive in waiting.

What the dangerous sink looks like

The pattern is short and grep-friendly. In Java:

// Anti-pattern: attacker-controlled reflective dispatch.
String className = request.getParameter("handler");   // untrusted
String method    = request.getParameter("op");        // untrusted
Object[] args    = decode(request.getParameter("a")); // untrusted

Class<?> c = Class.forName(className);                // arbitrary classpath type
Object  o = c.getDeclaredConstructor().newInstance();
Method  m = c.getDeclaredMethod(method, argTypes(args));
Object  r = m.invoke(o, args);                        // arbitrary side effects

The same shape in .NET (Activator.CreateInstance(Type.GetType(name))), Python (importlib.import_module(name) plus getattr), or Node (require(userSuppliedPath)) carries the same failure mode. Static analysis rules for these sinks exist in most SAST tools; the problem is that vulnerable projects typically classify them as intended behaviour and suppress the finding.

Controls that hold up

Four controls do the work; the rest is decoration.

  1. Allow-list the dispatch target. Enumerate the classes, methods, handler names, or hook paths a given endpoint may invoke, and reject everything else. If the list is dynamic, sign it. A safe reflective site knows at build time what it can ever call.
  1. Prefer schema-typed formats to object graphs. JSON or protobuf into a defined struct beats Java serialization, BinaryFormatter, pickle, or PHP unserialize on any endpoint that touches untrusted input. Where legacy formats must remain, disable polymorphic type handling.
  1. Separate configuration writes from runtime effects. A configuration endpoint should require authentication equal to the most sensitive setting it can change. Grouping cosmetic settings with settings that feed reflective loaders is how the PaperCut chain (CVE-2026-81578 into CVE-2026-82078) became a single unauthenticated primitive.
  1. Treat writable paths as a threat model. Any directory the runtime scans for plugins, hooks, or startup scripts needs a mediating write API with allow-listed filenames and content checks. CVE-2026-60004 and CVE-2026-66384 both reduce to a write outside a boundary the developer assumed was enforced by surrounding logic.

Detection

Reflective and deserialization sinks are noisy at runtime. Instrument the class loader (Java Agent, .NET profiler API, Python import hook) to record dispatch targets seen in production. Compare against the allow-list. Anything outside it is a bug or an intrusion. For hook-planting bugs, watch the filesystem: file-integrity monitoring on .git/hooks, /etc/cron.*, service-unit directories, and application plugin folders catches the deferred half of the exploit before the first invocation. Apply the same instrumentation to a staging deployment and diff the dispatch set week over week; a new class name in the log is a change-management event, not a debug curiosity.

The KEV entries above confirm these signals are worth wiring up before an advisory forces the conversation. Ship dispatch you can enumerate, formats you can type, configuration you can authenticate, and write paths you can mediate. Everything else is a CVE waiting for its catalog entry.

Verifiable security.