## Ticket: CVE-2026-40901 - DataEase Quartz `qrtz_job_details` Deserialization → RCE

**CVE**: CVE-2026-40901 | **CWE-502** (Deserialization of Untrusted Data) | **CVSS 3.1**: 8.8 (High)
**Vendor / Product**: DataEase (FIT2CLOUD) - Spring Boot data visualization platform
**Repository**: https://github.com/dataease/dataease
**Affected**: `<= v2.10.20` | **Fixed**: `v2.10.21`
**Writeup**: https://www.ox.security/blog/from-auth-bypass-to-rce-a-4-vulnerability-exploit-chain-in-dataease/
**NVD**: https://nvd.nist.gov/vuln/detail/CVE-2026-40901

### Impact

DataEase uses the Quartz scheduler with the **JDBC JobStore** backend so its
periodic tasks (refresh datasets, send digest emails, run maintenance) survive
restarts. Quartz persists each job's `JobDataMap` in the
`qrtz_job_details.JOB_DATA` column as a Java-serialized blob and calls
`ObjectInputStream.readObject()` on that blob every time the trigger fires.

The DataEase classpath ships a version of `commons-collections` that contains
the well-known transformer-based gadget chain. The only thing standing
between an attacker and code execution is the table contents — and the
preceding step in the chain (CVE-2026-40900, stacked SQL injection) hands
that to them. With write access to a single row of `qrtz_job_details`, the
next scheduler tick deserializes the attacker's payload and runs whatever
method chain it encodes in the JVM running the DataEase backend.

This is the terminal step of the 4-CVE chain (auth bypass → blocklist bypass
→ stacked SQLi → **Quartz deserialization RCE**). The end result is code
execution inside the DataEase container as the DataEase process user (root,
in the default container image).

### Affected / Fixed

| | Version |
|--|--|
| **Vulnerable** | DataEase community ≤ `v2.10.20` |
| **Fixed** | `v2.10.21` (https://github.com/dataease/dataease/releases/tag/v2.10.21) |

### Where the patch lives

```
git clone https://github.com/dataease/dataease external/dataease
cd external/dataease
git diff v2.10.20 v2.10.21 -- '**/*.java' '**/quartz*.properties' '**/pom.xml' 'core-backend/**' \
  | grep -E '(JobDataMap|useProperties|commons-collections|jobStore|deserializ|ObjectInputStream)'
```

Most likely patch surface (any one is sufficient to break the chain):

1. **commons-collections upgrade / removal** — bumping past 3.2.2 / 4.1 or
   removing the dependency removes the transformer gadgets.
2. **`org.quartz.jobStore.useProperties=true`** in `quartz.properties` —
   forces Quartz to serialize JobDataMap as a flat string-keyed property
   map (no Java serialization).
3. **Deserialization allow-list / `ObjectInputFilter`** — registered on the
   ObjectInputStream Quartz uses for JOB_DATA.

### Reproduction plan

The plan deliberately uses a **container-local marker file** as evidence —
no reverse shells, no outbound network from the container.

1. Bring up vulnerable DataEase + MySQL using the official compose pinned to
   `v2.10.20` (image `dataease/dataease:v2.10.20`). Note the MySQL
   credentials from `docker-compose.yml` (default
   `dataease / Password123@mysql`).
2. Pre-verify that `/tmp/pruva-cve-2026-40901.txt` does **not** exist inside
   the DataEase container: `docker exec <dataease> ls -l /tmp` should not
   list it.
3. Generate a serialized payload using a public reference such as the
   `ysoserial` project's `CommonsCollections` chain. The payload's
   embedded command is a single benign two-step:
   `touch /tmp/pruva-cve-2026-40901.txt && id >> /tmp/pruva-cve-2026-40901.txt`.
   Capture the bytes to `payload.bin`.
4. Connect to MySQL with the credentials from step 1 and run:
   ```sql
   UPDATE qrtz_job_details
   SET JOB_DATA = LOAD_FILE('/payload.bin')   -- or hex-encoded literal
   WHERE JOB_NAME LIKE 'dataset-refresh%' LIMIT 1;
   ```
   (Adjust the WHERE clause to whatever job exists in the freshly-installed
   instance; `SELECT JOB_NAME FROM qrtz_job_details` first.)
5. Wait up to one minute for the Quartz scheduler to fire the next tick.
6. Verify: `docker exec <dataease> cat /tmp/pruva-cve-2026-40901.txt`
   returns a non-empty file whose contents include `uid=0(root)`.
7. Stop the vulnerable stack. Bring up the **fixed** stack
   (`dataease/dataease:v2.10.21`). Repeat steps 2-6. The vulnerable build
   produces the marker file; the patched build leaves /tmp untouched (or
   throws a deserialization-rejected exception in the DataEase logs).

### Expected reproduction artifacts

- `reproduction_steps.sh` — orchestrates the two `docker compose` stacks,
  emits the `UPDATE` against each, polls for the marker file, asserts on
  the diff between vulnerable and fixed behavior.
- `validation_verdict.json` — `successful` only if the marker file appears
  on `v2.10.20` AND does not appear on `v2.10.21`.
- `evidence/` —
  - `marker-vulnerable.txt` — contents of `/tmp/pruva-cve-2026-40901.txt`
    from the v2.10.20 container (shows `uid=0(root)` etc.).
  - `marker-fixed.txt.absent` — empty sentinel file proving the marker
    never appeared on v2.10.21.
  - `dataease-fixed.log` — relevant log lines from the patched build
    showing the deserialization being rejected (if logged).
  - `qrtz-before.sql` and `qrtz-after.sql` — `SELECT JOB_NAME, LENGTH(JOB_DATA)`
    snapshots before/after the UPDATE on each stack.

### Hard rules for this reproduction

- **No reverse shells. No outbound network from the DataEase container.**
  Evidence must be a file inside `/tmp` of the DataEase container.
- The embedded command runs `touch` + `id`. Nothing else.
- After the run, the agent must `docker compose down -v` both stacks to
  leave no DataEase containers behind.
