This is a fire-escape door alarm on a building that bricked up the fire escape three years ago
CVE-2023-21939 is a sandbox-bypass flaw in the Swing (JLabel HTML-rendering) component of Oracle Java SE 8u361, 11.0.18, 17.0.6, 20 and GraalVM Enterprise Edition 20.3.9, 21.3.5, 22.3.1. By crafting a JLabel with embedded HTML <object> tags referencing org.apache.batik.swing.JSVGCanvas, an attacker can load a remote malicious SVG that triggers code execution via event listeners. Oracle describes the attack surface as *'clients running sandboxed Java Web Start applications or sandboxed Java applets that load and run untrusted code.'* The CVSS vector scores it AV:N/AC:L/PR:N/UI:N — implying trivial remote exploitation.
Oracle's MEDIUM / 5.3 rating was arguably fair *in 2023 on paper*, but even then it overstated real-world risk. The primary attack vector — Java applets and Web Start — has been dead in browsers since 2015–2017 (NPAPI removal in Chrome 45, Firefox 52, Edge launch). The Applet API was deprecated in JDK 9 and marked for removal in JDK 17. The secondary deserialization-chain vector (Y4Sec-Team PoC) requires Apache Batik-Swing and Mozilla Rhino on the classpath *plus* a deserializing endpoint accepting untrusted JLabel objects — a vanishingly rare stack in production. The integrity-only impact (I:L, no C or A) further limits blast radius. In September 2026, with three-plus years of patching cycles behind us and no KEV listing, no known campaigns, and EPSS at ~2.5%, this is a backlog item, not a priority.
4 steps from start to impact.
Identify a target running vulnerable Java + Swing
- Target runs an affected Java version
- Target application loads
javax.swingclasses
- Server-side Java deployments — the vast majority of enterprise Java — do not use Swing at all
- Most enterprises have patched Java at least once since April 2023, removing the vulnerable versions
Deliver untrusted code to the sandbox (Applet/Web Start path)
.jnlp or applet page and lures the victim to visit it. The Java sandbox is supposed to contain the code, but this CVE bypasses it to modify data.- Victim's browser supports NPAPI or Java Web Start
- Victim clicks a link and approves the Java security prompt
- No modern browser supports Java applets. Chrome removed NPAPI in 2015 (v45), Firefox in 2017 (v52). Edge never supported it.
- Java Web Start was deprecated in JDK 9 (2017) and is unavailable in JDK 11+
- Even on JDK 8 with Web Start, users must click through multiple security warnings
Deliver via deserialization chain (Y4Sec-Team PoC path)
JLabel object containing an HTML <object classid="org.apache.batik.swing.JSVGCanvas"> tag pointing to an attacker-controlled SVG. When the target application deserializes the object, Swing renders the HTML, Batik loads the SVG, and embedded JavaScript event listeners (via Rhino) execute arbitrary commands.- Target application deserializes untrusted Java objects
- Apache Batik-Swing (
batik-swing) is on the classpath - Mozilla Rhino scripting engine is on the classpath
- Outbound HTTP from the target to attacker infrastructure is allowed
- Deserializing untrusted Java objects is a well-known anti-pattern; modern frameworks use JSON/protobuf instead
- Batik-Swing is uncommon in production server stacks; it's an SVG rendering library for GUI apps
- The combination of Batik + Rhino + untrusted deserialization is extraordinarily rare
- Egress filtering / proxy requirements block the outbound SVG fetch in hardened environments
javax.swing.JLabel.Achieve integrity impact
- One of the two prior delivery paths succeeds
- Impact is scoped to I:L (low integrity) per Oracle's own CVSS vector
- RCE via the PoC requires additional third-party libraries not guaranteed to be present
The supporting signals.
| In-the-wild exploitation | None known. Not listed on CISA KEV. No public reports of active campaigns exploiting this CVE as of September 2026. |
|---|---|
| Proof-of-concept | Public. Y4Sec-Team/CVE-2023-21939 on GitHub. Extended PoC by win3zz on GitHub Gist demonstrating RCE via Batik+Rhino deserialization chain. |
| EPSS score | 0.02495 (~2.5%) — low probability of exploitation in the next 30 days. Well below the median for network-accessible vulns. |
| KEV status | Not listed. No CISA Known Exploited Vulnerability entry. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N — Network-accessible, no privileges/interaction, but only low integrity impact. No confidentiality or availability damage. |
| Affected versions | Oracle Java SE 8u361, 8u361-perf, 11.0.18, 17.0.6, 20; GraalVM Enterprise Edition 20.3.9, 21.3.5, 22.3.1. |
| Fixed versions | Java SE 8u371, 11.0.19, 17.0.7, 20.0.1; GraalVM EE 20.3.10, 21.3.6, 22.3.2. Distro backports: RHEL (RHSA-2023:1875), Debian (DSA-5388-1), Ubuntu (USN-6077-1), Amazon Linux (ALAS2-2023-2028). |
| Scanning / exposure | Shodan/Censys queries for exposed Java services are not directly applicable — the primary vector (applets) is client-side. Deserialization endpoints are not trivially fingerprinted externally. |
| Disclosure date | 2023-04-18, part of Oracle April 2023 Critical Patch Update. |
| Reporting researcher | Y4Sec Team (credited for public PoC research); original reporter to Oracle not publicly named. |
noisgate verdict.
The single most decisive factor is the extinction of the primary attack surface: Java applets and Web Start have been unsupported in all major browsers since 2017, eliminating the documented remote attack vector for virtually 100% of enterprise endpoints. The secondary deserialization path requires an implausible three-library intersection (Swing + Batik + Rhino + untrusted deser) that excludes mainstream server-side Java deployments.
Why this verdict
- Dead attack vector: The Oracle advisory explicitly scopes this to 'clients running sandboxed Java Web Start applications or sandboxed Java applets.' Every major browser dropped Java plugin support by 2017. This path is non-functional on modern endpoints.
- Implausible dependency chain for deserialization path: The PoC requires
batik-swing+ Rhino + an endpoint that deserializes untrustedJLabelobjects. This triple intersection is extraordinarily rare in production. Most enterprise Java is server-side (Spring, Jakarta EE) and never loads Swing. - Integrity-only, low impact: The CVSS vector is C:N/I:L/A:N. Even if exploited, the attacker modifies *some* data — no secrets leaked, no DoS, no privilege escalation per the CVE scope.
- Role multiplier: Java SE runs on CI/CD servers, app servers, and database tiers — all potentially high-value roles. However, Swing is a *desktop GUI toolkit*. High-value server roles (Jenkins, Tomcat, Spring Boot, Kafka) do not load Swing components or deserialize Swing objects. The blast radius on high-value roles is effectively nil because the vulnerable component (Swing JLabel HTML rendering) is not exercised in server deployments. No floor elevation applies.
- Three years unpatched with no exploitation: Disclosed April 2023, no KEV, no campaigns, EPSS at 2.5%. The security community and threat actors have had ample time; the lack of uptake confirms the theoretical nature of this risk.
Why not higher?
The vulnerability has a public PoC and is theoretically network-accessible without authentication, which normally warrants at least MEDIUM. However, the primary attack vector is extinct (applets), the secondary vector requires a near-impossible library combination in production, and the impact is limited to low integrity with no confidentiality or availability loss. Elevating to MEDIUM would overstate actionable risk for any modern enterprise.
Why not lower?
A public PoC exists and the deserialization chain, while improbable, is technically functional. Rare legacy environments (e.g., manufacturing or government kiosks still running Java 8 with Web Start) could theoretically be affected. Dropping to IGNORE would be inappropriate given that the PoC achieves command execution under specific conditions.
What to do — in priority order.
- Confirm Swing is not loaded in production JVMs — Run
jcmd <pid> VM.classloadersor check loaded classes forjavax.swingon your server fleet. If Swing is not loaded — which is the expected case for server-side Java — this CVE has zero applicability. No SLA applies for LOW; treat as backlog hygiene. - Audit deserialization endpoints — If any Java service accepts
ObjectInputStreamfrom untrusted sources, that is the real problem — not this specific CVE. Implement JEP 290 deserialization filters (jdk.serialFilter) globally. This addresses entire gadget-chain classes, not just CVE-2023-21939. - Remove Batik and Rhino from classpaths where not needed — If
batik-swingorrhinoappear in your dependency tree but are unused, remove them. This eliminates the PoC chain entirely. Usemvn dependency:treeorgradle dependenciesto audit. - Block outbound HTTP from application servers — The PoC requires the victim JVM to fetch a remote SVG over HTTP. Egress filtering via proxy or firewall rules breaks this chain.
- Upgrading browsers — browsers already don't support Java applets; upgrading them doesn't change the risk posture for this CVE.
- WAF rules for HTTP request patterns — the initial exploit delivery (deserialization) arrives via the application's own deserialization endpoint, not a standard HTTP exploit payload a WAF would pattern-match.
- Java Security Manager hardening — the Security Manager itself was deprecated in JDK 17 and is being removed. It was also the mechanism this CVE bypasses, so tightening it is circular.
Crowdsourced verification payload.
Run this on each target host where Java is installed. It checks the installed JDK/JRE version against the known-vulnerable ranges. No special privileges required beyond read access to the java binary. Example: bash check_cve_2023_21939.sh or bash check_cve_2023_21939.sh /usr/lib/jvm/java-11/bin/java
#!/usr/bin/env bash
# CVE-2023-21939 version checker
# Checks if installed Java version is in the vulnerable range
# Exit codes: 0 = PATCHED, 1 = VULNERABLE, 2 = UNKNOWN
JAVA_BIN="${1:-java}"
if ! command -v "$JAVA_BIN" &>/dev/null; then
echo "UNKNOWN - java binary not found at: $JAVA_BIN"
exit 2
fi
VERSION_OUTPUT=$("$JAVA_BIN" -version 2>&1)
echo "Detected: $VERSION_OUTPUT" | head -1
# Extract version string
VER=$(echo "$VERSION_OUTPUT" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
# Parse major version
MAJOR=$(echo "$VER" | sed -E 's/^1\.([0-9]+)\..*/\1/' | sed -E 's/^([0-9]+)\..*/\1/')
if echo "$VER" | grep -qE '^1\.'; then
MAJOR=$(echo "$VER" | cut -d. -f2)
else
MAJOR=$(echo "$VER" | cut -d. -f1)
fi
# Parse update number for JDK 8
UPDATE=0
if [ "$MAJOR" = "8" ]; then
UPDATE=$(echo "$VER" | sed -E 's/.*[_.]([0-9]+).*/\1/' || echo 0)
fi
# Parse minor for 11, 17, 20+
MINOR=$(echo "$VER" | cut -d. -f2)
PATCH=$(echo "$VER" | cut -d. -f3 | sed 's/[^0-9].*//')
case "$MAJOR" in
8)
if [ "$UPDATE" -le 361 ] 2>/dev/null; then
echo "VULNERABLE - Java 8 update $UPDATE (fixed in 8u371+)"
exit 1
else
echo "PATCHED - Java 8 update $UPDATE"
exit 0
fi
;;
11)
if [ "${MINOR:-0}" -eq 0 ] && [ "${PATCH:-0}" -le 18 ] 2>/dev/null; then
echo "VULNERABLE - Java 11.0.${PATCH} (fixed in 11.0.19+)"
exit 1
else
echo "PATCHED - Java $VER"
exit 0
fi
;;
17)
if [ "${MINOR:-0}" -eq 0 ] && [ "${PATCH:-0}" -le 6 ] 2>/dev/null; then
echo "VULNERABLE - Java 17.0.${PATCH} (fixed in 17.0.7+)"
exit 1
else
echo "PATCHED - Java $VER"
exit 0
fi
;;
20)
if [ "${MINOR:-0}" -eq 0 ] && [ "${PATCH:-0}" -eq 0 ] 2>/dev/null; then
echo "VULNERABLE - Java 20.0.0 (fixed in 20.0.1+)"
exit 1
else
echo "PATCHED - Java $VER"
exit 0
fi
;;
*)
if [ "$MAJOR" -gt 20 ] 2>/dev/null; then
echo "PATCHED - Java $MAJOR is not affected"
exit 0
else
echo "UNKNOWN - Unrecognized Java version: $VER"
exit 2
fi
;;
esacIf you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.