This is a valet key to your server’s internal driveway, not the keys to the whole building
CVE-2026-3733 is an SSRF in xxl-job's admin-side trigger flow, reported against versions up to 3.3.2. The vulnerable path is the /jobinfo/trigger handler in JobInfoController, where attacker-controlled addressList data is passed into the remoting utility that opens an outbound HttpURLConnection. In plain English: a logged-in XXL-JOB user can try to make the scheduler server send HTTP(S) requests to destinations of the attacker's choosing.
The vendor's 6.3/MEDIUM label is directionally fair, but it overstates the broad enterprise urgency if you treat this like an internet-edge bug. The decisive friction is that exploitation needs valid low-privileged application access to a management plane that is usually internal, and the impact is SSRF rather than direct code execution. That pushes this down from 'drop everything' territory, but not into ignore-land because scheduler nodes often sit on trusted network paths and can reach internal services your attacker cannot.
4 steps from start to impact.
Get an XXL-JOB session
#3924 uses a preexisting XXL_JOB_LOGIN_IDENTITY cookie and targets /xxl-job-admin/jobinfo/trigger, which lines up with the CVSS PR:L rating rather than unauthenticated exposure.- Valid low-privileged XXL-JOB account or stolen session
- Reachability to the
xxl-job-adminweb interface
- This is not pre-auth
- Many deployments keep the admin console on internal-only networks or behind VPN/SSO
- Credential theft or prior foothold is required first
Abuse /jobinfo/trigger with a user-controlled URL
requests PoC from GitHub issue #3924, the attacker submits addressList=<attacker URL or internal target> to the trigger endpoint. The vulnerable flow passes that value into XxlJobRemotingUtil.postBody, which constructs a URL and opens an outbound connection directly.- Authenticated access to the trigger action
- Ability to submit
addressListdata
- Application role checks may narrow who can trigger jobs in real deployments
- Some environments may require a known job ID or valid executor context
new URL(url) plus openConnection() on tainted input in the affected code path.Use the scheduler as an internal HTTP client
- Outbound connectivity from the scheduler host to the target destination
- Target service speaks HTTP or HTTPS and is reachable from the server
- Modern egress filtering, proxying, and metadata protections can break the exploit chain
- Non-HTTP internal services are out of scope
- Some targets still require their own authentication
xxl-job-admin hosts to RFC1918 ranges, link-local metadata IPs, or OAST domains such as dnslog/interactsh.Pivot into a second bug or leak internal data
- Interesting internal web targets exist behind the scheduler
- Those targets trust network location or expose sensitive unauthenticated endpoints
- SSRF alone is not code execution
- Many high-value targets still require authentication or mTLS
- Blast radius depends entirely on the scheduler host's placement and egress rights
The supporting signals.
| In-the-wild status | No KEV listing found and I found no trustworthy evidence of active exploitation campaigns in the source set. Treat this as *publicly known with public PoC*, not as a confirmed mass-exploitation event. |
|---|---|
| Proof-of-concept availability | Public PoC exists. GitHub issue #3924 includes a Python requests proof of concept from NinjaGPT showing addressList control and OOB confirmation. |
| EPSS | 0.00064 from the user-supplied intel block. That is *very low likelihood* territory; a commonly mirrored listing also reports it as higher than roughly 13% of CVEs, which is still low signal for near-term exploitation. |
| KEV status | Not listed in CISA KEV as of this assessment. No KEV date applies. |
| CVSS vector read | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L means network-reachable and easy to trigger *once authenticated*, with only low CIA impact scored in the base model. |
| Affected versions | xxl-job up to 3.3.2 per NVD/VulDB-backed record. The vulnerable area is the admin trigger flow in JobInfoController feeding XxlJobRemotingUtil. |
| Fixed version | No vendor-confirmed fix version was clearly documented in the retrieved source set. v3.4.0 exists, but I did not find an authoritative release note tying this CVE to a fix, so I will not overclaim patch status. |
| Scanning / exposure data | No source-backed public exposure count for this exact CVE was located in the retrieved GreyNoise/Censys/Shodan material. Operationally, assume external exposure is usually limited because xxl-job-admin is a scheduler console, then validate with your own ASM instead of internet-wide guesswork. |
| Disclosure timeline | Public issue opened 2026-02-25 on GitHub; CVE published 2026-03-08 in NVD. |
| Reporter / source | Public report and PoC were posted by NinjaGPT in GitHub issue #3924; the NVD record is sourced from VulDB. |
noisgate verdict.
The single biggest reason this stays out of HIGH is that it requires authenticated access to an internal management application before the attacker can do anything. That prerequisite makes this a post-initial-access pivot tool, not an internet-edge compromise path, even though SSRF from a scheduler node can still be dangerous when network placement is favorable.
Why this verdict
- Downward pressure: requires authenticated remote access to
xxl-job-admin, which implies the attacker already has an application foothold or stolen session before this CVE matters. - Downward pressure: exposure population is narrow because scheduler/admin consoles are typically internal, VPN-restricted, or otherwise not broadly internet-exposed in mature enterprise deployments.
- Downward pressure: SSRF is a pivot, not direct takeover; the base impact is bounded unless the scheduler can reach high-value internal HTTP targets or metadata services.
- Upward pressure: scheduler nodes are trust-rich and often sit on network paths that can see internal services ordinary users cannot, so a 'mere SSRF' can still unlock meaningful lateral visibility.
- Upward pressure: public PoC exists in the GitHub issue, lowering attacker effort once an account is obtained.
Why not higher?
This is not unauthenticated, not wormable, and not demonstrated as actively exploited. The need for valid app access plus the usual internal-only placement of xxl-job-admin cuts the reachable population sharply, which is exactly the kind of compounding friction that keeps a 10,000-host patch program from treating this like a front-burner internet crisis.
Why not lower?
It is still a real SSRF with a public PoC against a management-plane product, and management-plane servers tend to have attractive east-west reach. If an attacker already has a user in XXL-JOB, this bug can meaningfully expand visibility and access to internal HTTP services, so writing it off as backlog dust would be complacent.
What to do — in priority order.
- Put
xxl-job-adminbehind internal-only access — Restrict the console to VPN, bastion, or trusted management subnets so the auth prerequisite cannot be satisfied from the open internet. For a MEDIUM verdict there is no mitigation SLA; do this as normal hardening while you work within the 365-day remediation window. - Constrain scheduler egress — Block or tightly proxy outbound HTTP(S) from
xxl-job-adminhosts so arbitraryaddressListvalues cannot reach RFC1918 space, link-local metadata, or uncontrolled external OAST endpoints. For this MEDIUM finding there is no mitigation SLA; prioritize it with your standard egress-hardening backlog. - Harden cloud metadata access — If the scheduler runs in cloud, enforce IMDS protections such as IMDSv2 on AWS and equivalent metadata restrictions elsewhere to reduce the worst SSRF follow-on. Again, there is no mitigation SLA for MEDIUM; fold this into platform baseline controls before the remediation deadline.
- Monitor for abnormal outbound web traffic from scheduler nodes — Add detections for
xxl-job-admininitiating connections to internal web apps, metadata IPs, or OAST domains because SSRF often shows up as strange east-west HTTP from an app server that normally talks only to executors and databases. Use this as compensating visibility while patching lands inside the 365-day window. - Tighten app-role assignment — Review who can trigger jobs or access the admin console, and shrink low-privileged user populations to the smallest practical set. The exploit path starts with a real XXL-JOB identity, so reducing that population directly lowers risk.
- A generic perimeter WAF does not reliably solve this if the attacker is an authenticated internal or VPN user and the dangerous traffic is the server's outbound request.
- MFA helps with the prerequisite account theft problem, but it does not neutralize the SSRF once a legitimate session already exists.
- Endpoint antivirus on the scheduler host is weak protection here because the bug abuses normal Java networking, not a dropped malware binary.
Crowdsourced verification payload.
Run this on the target host that runs xxl-job-admin, or on an auditor workstation with read access to the deployed JAR/WAR tree or exploded application directory. Invoke it with python3 verify_xxl_job_cve_2026_3733.py /opt/xxl-job-admin or point it directly at a JAR/WAR such as python3 verify_xxl_job_cve_2026_3733.py /srv/app/xxl-job-admin-3.3.2.jar; standard user rights are usually enough if you can read the files.
#!/usr/bin/env python3
# verify_xxl_job_cve_2026_3733.py
# Detect likely exposure to CVE-2026-3733 in xxl-job / xxl-job-admin deployments.
# Exit codes:
# 0 = PATCHED
# 1 = VULNERABLE
# 2 = UNKNOWN
import os
import re
import sys
import zipfile
from pathlib import Path
AFFECTED_MAX = (3, 3, 2)
NAME_HINTS = ("xxl-job", "xxl-job-admin", "xxl-job-core")
POM_PATH_HINT = "META-INF/maven"
def parse_version(text):
m = re.search(r'(\d+)\.(\d+)\.(\d+)', text or '')
if not m:
return None
return tuple(int(x) for x in m.groups())
def fmt(ver):
return '.'.join(str(x) for x in ver) if ver else 'unknown'
def compare(v1, v2):
return (v1 > v2) - (v1 < v2)
def candidate_files(root: Path):
if root.is_file():
return [root]
found = []
for p in root.rglob('*'):
if p.is_file() and p.suffix.lower() in ('.jar', '.war'):
if any(h in p.name.lower() for h in NAME_HINTS):
found.append(p)
return found
def version_from_filename(path: Path):
return parse_version(path.name)
def version_from_zip(path: Path):
try:
with zipfile.ZipFile(path, 'r') as zf:
# Prefer Maven pom.properties files
for name in zf.namelist():
low = name.lower()
if POM_PATH_HINT.lower() in low and low.endswith('pom.properties') and any(h in low for h in NAME_HINTS):
data = zf.read(name).decode('utf-8', errors='ignore')
for line in data.splitlines():
if line.startswith('version='):
ver = parse_version(line.split('=', 1)[1].strip())
if ver:
return ver, f'{path}!{name}'
# Fallback to MANIFEST or embedded path names
for name in zf.namelist():
low = name.lower()
if low.endswith('manifest.mf'):
data = zf.read(name).decode('utf-8', errors='ignore')
for key in ('Implementation-Version:', 'Bundle-Version:', 'Specification-Version:'):
for line in data.splitlines():
if line.startswith(key):
ver = parse_version(line.split(':', 1)[1].strip())
if ver:
return ver, f'{path}!{name}'
# Last resort: infer from internal filenames
for name in zf.namelist():
if any(h in name.lower() for h in NAME_HINTS):
ver = parse_version(name)
if ver:
return ver, f'{path}!{name}'
except Exception:
return None, None
return None, None
def inspect_path(target: Path):
# Direct directories may contain pom.properties in exploded deployments
if target.is_dir():
for p in target.rglob('pom.properties'):
low = str(p).lower()
if POM_PATH_HINT.lower() in low and any(h in low for h in NAME_HINTS):
try:
data = p.read_text(encoding='utf-8', errors='ignore')
for line in data.splitlines():
if line.startswith('version='):
ver = parse_version(line.split('=', 1)[1].strip())
if ver:
return ver, str(p)
except Exception:
pass
for f in candidate_files(target):
ver, source = version_from_zip(f)
if ver:
return ver, source
ver = version_from_filename(f)
if ver:
return ver, str(f)
return None, None
def main():
if len(sys.argv) != 2:
print('UNKNOWN - usage: python3 verify_xxl_job_cve_2026_3733.py <path-to-app-dir-or-jar>')
sys.exit(2)
target = Path(sys.argv[1]).expanduser().resolve()
if not target.exists():
print(f'UNKNOWN - path not found: {target}')
sys.exit(2)
version, source = inspect_path(target)
if not version:
print('UNKNOWN - could not determine xxl-job version from files')
sys.exit(2)
if compare(version, AFFECTED_MAX) <= 0:
print(f'VULNERABLE - detected xxl-job version {fmt(version)} from {source} (affected <= 3.3.2)')
sys.exit(1)
# No authoritative fixed-version statement was confirmed in the research set.
# Versions above 3.3.2 are treated as PATCHED for operational triage, but validate against vendor guidance.
print(f'PATCHED - detected xxl-job version {fmt(version)} from {source} (above affected range <= 3.3.2)')
sys.exit(0)
if __name__ == '__main__':
main()
If you remember one thing.
xxl-job-admin instance, confirm whether it is externally reachable, and review whether those hosts can reach internal web apps or cloud metadata. Because this is MEDIUM, there is no noisgate mitigation SLA — go straight to the 365-day remediation window; if you cannot prove safe placement, apply access restriction and egress controls as normal hardening while scheduling the vendor remediation or version upgrade inside the noisgate remediation SLA of ≤365 days. If your environment shows internet exposure plus broad egress from scheduler hosts, treat those specific instances as locally urgent even though the global CVE verdict stays MEDIUM.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.