← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-3733 · CWE-918 · Disclosed 2026-03-08

A vulnerability was detected in xuxueli xxl-job up to 3

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

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.

"Authenticated SSRF on an internal scheduler is real, but it is mostly a post-compromise pivot, not an edge-fire."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Get an XXL-JOB session

The attacker first needs a valid XXL-JOB login or session cookie. The public PoC in issue #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.
Conditions required:
  • Valid low-privileged XXL-JOB account or stolen session
  • Reachability to the xxl-job-admin web interface
Where this breaks in practice:
  • 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
Detection/coverage: Most network scanners will find the product, but not the auth requirement. Web logs and IdP logs are your best evidence for this stage.
STEP 02

Abuse /jobinfo/trigger with a user-controlled URL

Using the public Python 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.
Conditions required:
  • Authenticated access to the trigger action
  • Ability to submit addressList data
Where this breaks in practice:
  • Application role checks may narrow who can trigger jobs in real deployments
  • Some environments may require a known job ID or valid executor context
Detection/coverage: DAST can often catch this with OAST payloads. SAST should flag new URL(url) plus openConnection() on tainted input in the affected code path.
STEP 03

Use the scheduler as an internal HTTP client

If accepted, the XXL-JOB admin server sends a server-side HTTP(S) request to the chosen destination. That enables classic SSRF outcomes: internal service discovery, cloud metadata probing, access to admin-only HTTP endpoints, or reaching executor-local services that are not exposed externally.
Conditions required:
  • Outbound connectivity from the scheduler host to the target destination
  • Target service speaks HTTP or HTTPS and is reachable from the server
Where this breaks in practice:
  • 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
Detection/coverage: Look for unusual outbound requests from xxl-job-admin hosts to RFC1918 ranges, link-local metadata IPs, or OAST domains such as dnslog/interactsh.
STEP 04

Pivot into a second bug or leak internal data

SSRF is usually a bridge, not the destination. The attacker uses the scheduler's network position to enumerate internal applications, hit sensitive web consoles, or chain into metadata/token theft if cloud hardening is weak.
Conditions required:
  • Interesting internal web targets exist behind the scheduler
  • Those targets trust network location or expose sensitive unauthenticated endpoints
Where this breaks in practice:
  • 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
Detection/coverage: Behavioral detections matter more than signature detections here: unexpected east-west HTTP from scheduler nodes, unusual DNS beacons, and metadata endpoint access attempts.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo 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 availabilityPublic PoC exists. GitHub issue #3924 includes a Python requests proof of concept from NinjaGPT showing addressList control and OOB confirmation.
EPSS0.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 statusNot listed in CISA KEV as of this assessment. No KEV date applies.
CVSS vector readCVSS: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 versionsxxl-job up to 3.3.2 per NVD/VulDB-backed record. The vulnerable area is the admin trigger flow in JobInfoController feeding XxlJobRemotingUtil.
Fixed versionNo 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 dataNo 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 timelinePublic issue opened 2026-02-25 on GitHub; CVE published 2026-03-08 in NVD.
Reporter / sourcePublic report and PoC were posted by NinjaGPT in GitHub issue #3924; the NVD record is sourced from VulDB.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to MEDIUM (4.8/10)

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.

HIGH Authenticated access is required for exploitation
MEDIUM Impact is environment-dependent and rises if scheduler hosts can reach sensitive internal services or cloud metadata
LOW Vendor-fixed version could not be confirmed from authoritative release notes in the retrieved source set

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.

05 · Compensating Control

What to do — in priority order.

  1. Put xxl-job-admin behind 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.
  2. Constrain scheduler egress — Block or tightly proxy outbound HTTP(S) from xxl-job-admin hosts so arbitrary addressList values 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.
  3. 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.
  4. Monitor for abnormal outbound web traffic from scheduler nodes — Add detections for xxl-job-admin initiating 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.
  5. 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.
What doesn't work
  • 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.
06 · Verification

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.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/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()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: identify every 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

  1. NVD CVE-2026-3733
  2. GitHub issue #3924 SSRF Vulnerability
  3. XXL-JOB releases
  4. XXL-JOB English documentation
  5. CISA Known Exploited Vulnerabilities Catalog
  6. FIRST EPSS API
  7. Project repository
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.