Skip to content

CVE-2026-70426: Verified Reproduction

CVE-2026-70426: Jenkins Remoting JEP-200 deserialization filter bypass SECURITY-3911 : unfiltered ClassNotFoundException fallback in MultiClassLoaderSerializer.resolveClass and ObjectInputStreamEx.resolveClass lets agents deserialize blocked core-classpath classes on the controller

CVE-2026-70426 is verified against the affected target. Vulnerability class: Deserialization. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00322.

REPRO-2026-00322 Deserialization Aug 23, 2026 CVE entry .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
162m 43s
Tool calls
355
Spend
$17.33
01 · Overview

What Is CVE-2026-70426?

CVE-2026-70426 is a critical-severity Deserialization vulnerability. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00322).

02 · Severity & CVSS

CVE-2026-70426 Severity

CVE-2026-70426 is rated critical severity.

CRITICAL threat level
Weakness CWE-502 — Deserialization of Untrusted Data

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

How to Reproduce CVE-2026-70426

$ pruva-verify REPRO-2026-00322
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00322/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-70426

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

serialized hudson.remoting.UserRequest whose Callable bytes are produced by a SpoofedTagSystemClassLoaderOutput (TAG_SYSTEMCLASSLOADER annotation) naming the JEP-200-blocked class hudson.security3911.Payload

Attack chain
  1. agent
  2. JNLP4-connect handshake
  3. hudson.remoting.Channel
  4. UserRequest.perform
  5. UserRequest.deserialize
  6. MultiClassLoaderSerializer$Input.resolveClass
  7. ClassNotFoundException fallback
  8. super.resolveClass (unfiltered, pre-fix)
  9. Payload.readObject executes /bin/sh on controller JVM
How the agent worked 766 events · 355 tool calls · 2h 43m
2h 43mDuration
355Tool calls
132Reasoning steps
766Events
13Dead-ends
Agent activity over 2h 43m
Policy
1
Support
5
Repro
451
Judge
52
Variant
252
Verify
1
0:00162:35

Root Cause and Exploit Chain for CVE-2026-70426

Versions: Remoting <= 3384.v60d89463d9e0 (except backport 3355.3357.v931d3c992987);Fixed: Remoting 3385.vf1123fb_515da_ (Jenkins 2.576 / LTS 2.568.2).

Jenkins Remoting (agent–controller communication library) contains two unfiltered ClassNotFoundException fallback paths in its deserialization class-resolution code. In hudson.remoting.MultiClassLoaderSerializer.Input.resolveClass() (and identically in hudson.remoting.ObjectInputStreamEx.resolveClass()), the primary path resolves the incoming class name against the channel-annotated classloader and then applies the JEP-200 class filter (channel.classFilter.check(c)). When that lookup throws ClassNotFoundException, the fallback super.resolveClass(desc) resolved the class via the receiving JVM's own classloader without applying the class filter. An attacker able to speak the agent protocol (Agent/Connect permission, a compromised agent, or code running on an agent) can therefore deserialize a JEP-200-blocked class on the Jenkins controller by serializing it with a spoofed TAG_SYSTEMCLASSLOADER annotation, forcing the ClassNotFoundException fallback. The blocked class's readObject then executes in the controller JVM, yielding remote code execution on the controller.

  • Package/component: org.jenkins-ci.main:remoting (Jenkins Remoting), embedded in Jenkins core (jenkins.war).
  • Affected: Remoting <= 3384.v60d89463d9e0 (except backport 3355.3357.v931d3c992987); Jenkins weekly <= 2.575; LTS <= 2.568.1.
  • Fixed: Remoting 3385.vf1123fb_515da_ (Jenkins 2.576 / LTS 2.568.2).
  • Risk: Critical (CVSS 3.1 9.6, AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H) — agent-to-controller RCE. AC:H because the gadget class must live on the controller's core classpath and evade the pre-JEP-200 static denylist (which this bypass does not defeat).

Impact Parity

  • Disclosed/claimed maximum impact: arbitrary code execution on the Jenkins controller from the agent side of a Remoting channel (RCE).
  • Reproduced impact from this run: OS command execution on the Jenkins controller JVM across the real JNLP4-connect (TCP) channel, proven twice by two independent direct OS-level observations per attempt: (1) /bin/sh -c executed on the controller writing a marker file with id/hostname output inside the controller container (uid=0(root) ... jvm_pid=...), and (2) an outbound TCP callback from the controller JVM to an attacker-controlled listener carrying the per-attempt token and the controller hostname.
  • Parity: full.
  • Not demonstrated: nothing material; the ClassCastException after readObject detonation is an artifact of the minimal PoC gadget (Serializable-only), not a limitation of the bypass. A real-world exploit would use a gadget class already on the core classpath whose readObject performs the malicious action (exactly the pattern this PoC models).

Root Cause

src/main/java/hudson/remoting/MultiClassLoaderSerializer.java (vulnerable line 137):

} catch (ClassNotFoundException ex) {
    return super.resolveClass(desc);        // <-- no channel.classFilter.check(...)
}

and identically src/main/java/hudson/remoting/ObjectInputStreamEx.java line 64:

} catch (ClassNotFoundException ex) {
    return super.resolveClass(desc);        // <-- no filter.check(...)
}

The name-based check channel.classFilter.check(name) still runs first, so classes on the pre-JEP-200 static denylist (e.g. commons-collections functors) remain blocked; but the class-level JEP-200 check (jenkins.security.ClassFilterImpl.isBlacklisted(Class)), which rejects classes whose code location is not Jenkins core/Remoting/a plugin and which are not in whitelisted-classes.txt, is skipped on the fallback path. An attacker forces the fallback by writing TAG_SYSTEMCLASSLOADER (-3) as the class annotation: the receiver then tries Class.forName(name, false, null) (bootstrap loader), which throws ClassNotFoundException for any non-JDK class, and the fallback resolves the class through the deserializing frame's classloader (on a real controller, the Jetty webapp classloader, which can see the whole core classpath and delegates to the application classloader) — completely bypassing the JEP-200 decision.

Fix commit: f1123fb515da74560db60645539019cfa77bce49 (jenkinsci/remoting, released as 3385.vf1123fb_515da_): both fallbacks became return channel.classFilter.check(super.resolveClass(desc)); / return filter.check(super.resolveClass(desc));. Diff captured in bundle/repro/security3911-fix.diff; vulnerable/fixed source lines captured in bundle/logs/vuln_unfiltered_fallback.txt and bundle/logs/fixed_filtered_fallback.txt.

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; uses only Docker images jenkins/jenkins:2.575-jdk21, jenkins/jenkins:2.576-jdk21, maven:3.9-eclipse-temurin-21, alpine:3.22 plus the harness sources in bundle/repro/harness/).
  2. What it does:
    • Records the image digests and the war manifests (Remoting-Embedded-Version: 3384.v60d89463d9e0 for 2.575, 3385.vf1123fb_515da_ for 2.576) and the real fix-commit diff from a jenkinsci/remoting checkout (verifying the exact vulnerable and fixed lines).
    • Extracts the byte-identical remoting-3384.v60d89463d9e0.jar from the vulnerable war and compiles the PoC gadget (hudson.security3911.Payload, Serializable, readObject executes /bin/sh + outbound callback) into payload.jar, and the attack agent.
    • For each of two vulnerable and two fixed attempts: boots a real Jenkins controller (fresh JENKINS_HOME, setup wizard disabled, inbound-agent TCP listener on port 50000, production JnlpSlaveAgentProtocol4 accept path) with payload.jar on the controller's own launch classpath (a harness jar, as sanctioned by the ticket's reproduction requirements — the filter, channel, and UserRequest.deserialize path are 100% product code). init.groovy.d creates the agent1 inbound node, prints its JNLP secret, and proves the production JEP-200 filter identity: SECURITY3911_CHANNEL_DEFAULT_FILTER=jenkins.security.ClassFilterImpl and SECURITY3911_FILTER_PROBE=REJECTED: Rejected: hudson.security3911.Payload (i.e. the production filter blocks the payload class at class level on both builds).
    • The attack agent performs the real JNLP4-connect handshake using the production negotiation classes (JnlpAgentEndpoint, JnlpProtocolHandlerFactory, JnlpProtocol4Handler, IOHub, PublicKeyMatchingX509ExtendedTrustManager), builds a genuine hudson.remoting.UserRequest, replaces its serialized request bytes with bytes produced by a SpoofedTagSystemClassLoaderOutput (exactly the regression-test helper from the fix commit), and sends it over the established channel. The controller's DefaultJnlpSlaveReceiver.afterChannel/SlaveComputer.setChannel production flow runs on the same connection.
  3. Expected evidence:
    • Vulnerable (2.575): Payload.readObject executes on the controller during UserRequest.deserialize → marker file /tmp/CONTROLLER_PWNED_<token>.txt inside the controller container (contents include uid=0(root) and the controller hostname) AND an outbound callback SECURITY3911_CALLBACK token=... phase=readObject controller_host=<controller> received by the attacker's listener.
    • Fixed (2.576): SecurityException: Rejected: hudson.security3911.Payload; see https://jenkins.io/redirect/class-filter/ returned over the channel; no marker, no callback.

Evidence

  • Full run log: bundle/logs/reproduction_steps.log (two consecutive full runs, both OVERALL=0, exit code 0).

  • Per-attempt controller and agent logs: bundle/logs/attempt-{vuln,fixed}-{1,2}/{controller,agent}.log.

  • Direct OS-execution markers (collected from inside the controller containers via docker exec): bundle/logs/attempt-vuln-1/CONTROLLER_PWNED_security3911-vuln-1-*.txt and bundle/logs/attempt-vuln-2/CONTROLLER_PWNED_security3911-vuln-2-*.txt, e.g.:

    SECURITY3911_MARKER token=security3911-vuln-1-1786004490 phase=readObject
    uid=0(root) gid=0(root) groups=0(root)
    dc3492613861            <- controller container hostname
    jvm_pid=100
    
  • Outbound callback (agent log): CALLBACK_RECEIVED from=/172.19.0.2:38064 msg=SECURITY3911_CALLBACK token=security3911-vuln-1-1786004490 phase=readObject controller_host=dc3492613861 — the source IP and hostname belong to the controller container, proving the controller JVM executed attacker code and dialed out.

  • Vulnerable-channel exception (expected, post-detonation): ClassCastException: class hudson.security3911.Payload cannot be cast to class hudson.remoting.Callable (hudson.security3911.Payload is in unnamed module of loader 'app'; ...) — confirms the fallback resolved the class via the controller-side loader.

  • Fixed-channel rejection: EXCEPTION=Error: Failed to deserialize the Callable object. <- SecurityException: Rejected: hudson.security3911.Payload; see https://jenkins.io/redirect/class-filter/ with CALLBACK_COUNT=0 and no marker.

  • Production filter provenance (controller log, both builds): SECURITY3911_CHANNEL_DEFAULT_FILTER=jenkins.security.ClassFilterImpl, SECURITY3911_FILTER_PROBE=REJECTED: Rejected: hudson.security3911.Payload, and the JUL line jenkins.security.ClassFilterImpl#notifyRejected: hudson.security3911.Payload in file:/opt/harness/payload.jar might be dangerous, so rejecting.

  • Negative control: bundle/repro/negative_control_observation.json (both fixed attempts: rejection observed, command_executed=false, zero callbacks, zero marker files).

  • Environment: Docker 29.1.3; jenkins/jenkins:2.575-jdk21 (@sha256:16778c994cfc..., Remoting 3384.v60d89463d9e0) and jenkins/jenkins:2.576-jdk21 (@sha256:8c1c7e28b463..., Remoting 3385.vf1123fb_515da_); OpenJDK 21 controllers/agents; harness SHA-256s in bundle/logs/harness_sha256.txt.

Recommendations / Next Steps

  • Upgrade to Jenkins 2.576 / LTS 2.568.2 (Remoting 3385.vf1123fb_515da_) or the 3355.3357.v931d3c992987 backport line.
  • The fix is correct and minimal: route both ClassNotFoundException fallbacks through the channel's class filter, mirroring the primary path.
  • Defense in depth: restrict Agent/Connect, isolate agents, and monitor for Rejected: ... class-filter log lines plus unexpected outbound connections from the controller.
  • Testing: the fix commit's regression tests (ClassFilterTest multiClassLoaderSerializer_spoofedSystemClassLoader_isRejected and objectInputStreamEx_emptyClassLoader_fallbackIsFiltered) cover both fallbacks; this run reproduces the same spoof over the real JNLP4/TCP production path.

Additional Notes

  • Idempotency: the script runs two clean attempts per role with fresh JENKINS_HOME per attempt, unique per-attempt tokens, a fresh Docker network per run, and full container cleanup; two consecutive end-to-end runs both passed (OVERALL=0).
  • The second vulnerable fallback (ObjectInputStreamEx.resolveClass) is exercised when the remote peer does not advertise multi-classloader RPC capability; it is fixed by the same commit and follows the identical pattern (documented in the diff); the primary proof uses the MultiClassLoaderSerializer path, which is the default for JNLP4 agent channels.
  • The harness payload class implements only Serializable (not hudson.remoting.Callable) because the controller's webapp/application classloader split would otherwise fail linking; this mirrors a real gadget whose effect lives in readObject, and it makes the proof stronger: execution happens during deserialization, before any Callable cast or arbitrary-callable permission check.
  • Fresh Jenkins installs do not auto-install bundled detached plugins; the script installs the war-bundled instance-identity.hpi + bouncycastle-api.hpi into JENKINS_HOME/plugins (what the setup wizard would do) so the production JNLP4 TLS listener is functional.

CVE-2026-70426 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:002:54
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-70426 · REPRO-20
0:27
0:27
extract_facts
no facts extracted
0:30
0:30
supportclaim_contract
0:39
0:39
0:39
0:42
0:42
0:46
0:46
0:46
0:46
0:53
0:54
$ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/ 2>/dev/null; ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/repo-mirrors 2>/dev/null; ls bundle/.pruva bundle/learning bundle/repro bundle/logs 2>/dev/null; ls bundle/security3911-fix.diff /bundle/security3911-fix.diff 2>/dev/null; find / -maxdepth 3 -name "security3911*" 2>/dev/null | head
EXPLOIT-KNOWLEDGE PREFLIGHT REVIEW REQUIRED: `run_shell` is blocked until this agent calls `review_exploit_knowledge_preflight` with snapshot_sha256 `5c4e0966cb1b272734cc6bcbd9d072a03b5a97abdc07a1c3e317be2ae604d677` and exactly one disposition for each candidate ID [1e2efebc-e4fc-4a9c-924f-373bce278d55, ecfee63a-d34e-4dfe-94a3-19819daab982]. Historical records remain reference-only; review their typed mechanisms and preconditions before continuing.
0:54
error

Unknown error

0:59
1:05
1:05
$ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/ 2>/dev/null; echo ---; ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/repo-mirrors 2>/dev/null; echo ---; ls bundle/.pruva bundle/learning bundle/repro bundle/logs 2>/dev/null; echo ---; find /data/pruva/runs/c7c62461-b0d3-4efb-b1a7-557c4b280680 -name "security3911*" 2>/dev/null
repo-mirrors
---
---
bundle/.pruva:
run_attempt.json

bundle/learning:
exploit_knowledge_preflight.json
1:13
1:14
$which docker java mvn curl git 2>/dev/null; echo ---; java -version 2>&1; echo ---; docker info 2>&1 | head -20; echo ---; ls /data/pruva/project-cache/.run-views/95749f0a330dcee41e3ce349a76c8982d1c720582125092d8dd101e6a3a0fe5d/repo-mirrors/
1.5s
/usr/bin/docker
/usr/bin/curl
/usr/bin/git
---
/bin/bash: line 1: java: command not found
---
WARNING: Error loading config file: open /home/pruva/.docker/config.json: permission denied
Client:
1:16
$docker images 2>/dev/null; echo ---; docker ps -a 2>/dev/null
0.2s
{"cwd":"/data/pruva/runs/c7c62461-b0d3-4efb-b1a7-557c4b280680","duration_ms":192,"exit_code":0,"stderr":"","stdout":"IMAGE                                                                                                   ID             DISK USAGE   CONTENT SIZE   EXTRA\nalpine:3.22                                                                                             14358309a308       12.8MB         3.88MB        \ndocker:27-dind-rootless@sha256:e2ac8e8f66ae21a060b0a8e3005c70f6ed9441aabf409434463d1f6eecd38026         e2ac8e8f66ae        607MB          159MB        \nghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1   8096b2518d60       2.16GB          553MB        \ngradle:4.10.3-jdk8                                                                                      393cd6c24d69        1.1GB          324MB        \njenkins/jenkins:2.575-jdk21                                                                             16778c994cfc        747MB          263MB        \njetbrains/teamcity-server:2025.11.6                                                                     91c92f1010fc       4.19GB          1.8GB   U    \njetbr… [truncated]
2:46
2:47
$curl -sI --max-time 10 https://github.com 2>&1 | head -3; echo ---; curl -sI --max-time 10 https://registry-1.docker.io/v2/ 2>&1 | head -3; echo ---; curl -sI --max-time 10 https://get.jenkins.io 2>&1 | head -3
0.8s
HTTP/2 200 
date: Thu, 06 Aug 2026 06:30:04 GMT
content-type: text/html; charset=utf-8
---
HTTP/2 401 
date: Thu, 06 Aug 2026 06:30:04 GMT
content-type: application/json
---
2:54
2:54
08 · How to Fix

How to Fix CVE-2026-70426

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-70426 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-70426

Is CVE-2026-70426 exploitable?

Yes. Pruva independently reproduced CVE-2026-70426 and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00322).

How severe is CVE-2026-70426?

CVE-2026-70426 is rated critical severity.

What type of vulnerability is CVE-2026-70426?

CVE-2026-70426 is classified as CWE-502 (Deserialization of Untrusted Data), a Deserialization vulnerability.

How can I reproduce CVE-2026-70426?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-70426 reproduction verified?

Yes. Pruva reproduced CVE-2026-70426 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-70426

Authoritative sources for CVE-2026-70426 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.