The Constructor Was the Endpoint: Jenkins Stapler Data Binding
POST /descriptorByName/.../configure looked ordinary in the access log. The response was a redirect, not a crash. No shell metacharacters, serialized Java stream, or suspicious upload crossed the proxy. Yet a configuration object had been created that the receiving field was never supposed to accept.
That is the useful way to read the Jenkins security advisory published on September 2, 2026. It contains 33 CVE identifiers across Jenkins core, Stapler, plugins, and update-center2. The headline RCE matters, but the recurring mechanism matters more: form submission was treated as data while object construction quietly behaved like capability invocation.
This article examines that mechanism using the public advisory, Jenkins developer documentation, and a loopback-only model. It does not reproduce vendor exploit routes. The opening request is a composite lab scenario, not a reported incident.
The form was already an object graph
Jenkins does not flatten every configuration form into a bag of strings. Its structured form submission builds a JSON object tree in the browser and sends it in a hidden field. Stapler then binds that tree to Java objects. Plugin developers mark constructors with @DataBoundConstructor and optional setters with @DataBoundSetter.
Jenkins Stapler data binding is productive engineering. A nested UI maps cleanly to a nested configuration model. It also means a request can influence three security-sensitive decisions: which concrete class is selected, which constructor runs, and which setters execute afterward. A permission check around the final “save” operation does not necessarily protect side effects that occur while Stapler is still assembling the object graph.
CVE-2026-84647 exposed the broad version of this error. According to the advisory, Stapler versions included in Jenkins 2.579 and earlier, and LTS 2.568.2 and earlier, did not restrict instantiated objects to types compatible with the expected field type. Jenkins 2.580 and LTS 2.568.3 added that compatibility restriction. The patch moves the invariant ahead of construction: an unrelated class should never reach its constructor.

One primitive, several consequences
The September advisory reads like a map of what constructors and setters can do when reached through an unintended binding path.
- Core object reachability: CVE-2026-84645 allowed objects intended to own independent top-level configuration files to appear as nested values. A crafted object combination could expose an improperly protected Script Console and produce RCE.
- Outbound connections: CVE-2026-84662 involved an LDAP plugin constructor that connects to a supplied URL. CVE-2026-84664 reached GitLab connection setters and could send an already configured API token to an attacker-controlled endpoint.
- Global security state: CVE-2026-84658 and CVE-2026-84659 covered Script Security binding paths that could disclose approval configuration or disable forced Groovy sandboxing.
- File placement: CVE-2026-84668 could overwrite SAML identity-provider metadata, enabling authentication as arbitrary users. CVE-2026-84671 used an insufficiently validated file-parameter name to write outside the intended location, potentially reaching RCE.
- Authorization identity: CVE-2026-84672 granted Microsoft Entra ID group permissions by both object ID and non-unique display name. A tenant member able to create a lookalike group could inherit a privileged Jenkins grant.
These are not interchangeable bugs. Some require Overall/Read, some require access to specific configuration forms, and plugin-specific cases have different prerequisites. The common review question is still powerful: what happens before the intended authorization boundary believes it is operating on a valid object?
A safe mechanism lab
I ran a local Python model that contains no Jenkins routes, product classes, filesystem writes, or external network traffic. The “dangerous” constructor appends an event to an in-memory list. The payload selects a registered class that is unrelated to the field’s expected base type.
def vulnerable_bind(payload):
cls = REGISTRY[payload["$class"]]
return cls(**payload["args"])
def fixed_bind(payload, expected):
cls = REGISTRY[payload["$class"]]
if not issubclass(cls, expected):
raise TypeError("incompatible bound type")
return cls(**payload["args"])
The vulnerable path recorded constructor_side_effect:http://127.0.0.1:9/lab-only. The fixed path raised TypeError, and the event list remained empty. The result is intentionally modest. It proves control-flow ordering, not exploitability of any Jenkins endpoint.
Compatibility is necessary, but it is not a universal allowlist. A dangerous attacker-selected subtype may still satisfy isAssignableFrom. Robust plugin code should keep binding targets narrow, avoid attacker-controlled arbitrary class names, allow only intended implementations, and remove network, file, credential, or policy side effects from constructors. Validation that runs after construction is already late.
How an operator would chain it
In an authorized assessment, the first move is not to spray every descriptorByName route. Inventory core and plugin versions, then map low-privilege identities to configuration surfaces they can actually submit. The interesting asymmetry is a form that appears local to a job, agent, header, report, or identity provider but instantiates a global or side-effecting object.
A disciplined chain has four gates. First, confirm the principal can render and submit the target form. Second, alter only the concrete-type discriminator while keeping the rest of the request valid. Third, use a harmless canary such as a loopback URL, controlled DNS label, or temporary lab path. Fourth, prove that the constructor or setter ran before the request was rejected or before the configuration was committed. Stop there unless the rules of engagement explicitly authorize impact demonstration.
The quiet signal is often more valuable than a 500 response: a DNS lookup from the controller, a new temporary file, a changed config.xml, or a security setting that flips and then appears unchanged in the submitting user’s UI. This resembles the trust failures behind TeamCity deserialization, but the payload grammar and privilege model differ. It also rhymes with Gitea’s patch-to-hook path: an apparently declarative operation crosses into execution because a hidden lifecycle stage carries more authority than reviewers assumed.
Detection must join HTTP to controller behavior
Single-layer telemetry will miss the story. Reverse-proxy logs see a legitimate configuration POST. Controller logs may show only a descriptor or binding exception. Egress controls see LDAP, GitLab, or HTTP traffic without the user action that caused it. Correlate them.
- Alert on configuration POSTs by principals whose normal role is read, build, or job-local administration, especially when the response is followed by a binding exception.
- Baseline controller egress by destination and initiating plugin. CI controllers should not discover new identity, SCM, or webhook endpoints during ordinary job edits.
- Version and monitor
JENKINS_HOMEconfiguration files. High-value diffs include security realm metadata, script-security policy, agent secrets, global connections, backup paths, and appearance resources. - Record plugin installation and version state with the event. “Jenkins patched” is incomplete when the reachable constructor belongs to an independently released plugin.
- Treat unexpected Script Console access, sandbox-policy changes, or approval-list reads as a chain, not isolated administration noise.
Content Security Policy can reduce the impact of several stored XSS cases in the advisory, but it does not repair unsafe object construction. Jenkins documents CSP for user-controlled content separately because it is a rendering boundary, not a substitute for binding or authorization controls.
What to change in reviews
JEP-200 moved XStream and Remoting from a denylist toward an allowlist because deciding what may deserialize scales better than enumerating every dangerous type. The September bugs show the same design pressure in Stapler data binding. Security review must cover all object-creation grammars, not only Java serialization.
For maintainers, search for @DataBoundConstructor and @DataBoundSetter, then classify each by side effect, required permission, accepted subtype set, secret access, and persistence target. For defenders, patch Jenkins core to 2.580 or LTS 2.568.3 and update affected plugins to the fixed versions listed in the advisory. Do not restore legacy behavior flags without recording an expiry and compensating control.
The strategic lesson is precise: a configuration parser that chooses classes is an execution dispatcher. Put authorization, type constraints, and allowed-class policy before dispatch. Then design constructors as if untrusted input will eventually find them, because in extensible control planes, it usually does.
Sources
- Jenkins Security Advisory 2026-09-02, published September 2, 2026.
- Jenkins developer documentation: Structured Form Submission.
- JEP-200: Switch Remoting/XStream denylist to an allowlist.
- Jenkins Stapler source repository.
- Jenkins documentation: Configuring Content Security Policy.
💜 Enjoyed this content? Support the blog with USDT (TRC20):
TX7obcjHQbDUXb4mGqoASEu1QFTKT2CFGG
