## Ticket: CVE-2026-40900 - DataEase `previewSql` Stacked SQL Injection

**CVE**: CVE-2026-40900 | **CWE-89** (SQL Injection) | **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-40900

### Impact

DataEase's "preview SQL" endpoint takes user-supplied SQL meant to represent a
dataset's source query and wraps it inside a subquery, roughly:

```
SELECT * FROM ( <USER_SQL> ) AS pruva_alias LIMIT 100
```

The server does NOT validate that the user input is a single `SELECT`
statement. When the underlying MySQL JDBC connection has
`allowMultiQueries=true` (set on the JDBC URL of the configured datasource), a
crafted payload using MySQL's `#` comment terminator can escape the wrapping
subquery and execute arbitrary stacked statements. Example payload:

```
SELECT 1 FROM dual) AS x; INSERT INTO core_msg_type (id, name, pid) VALUES (999999999, 'pwned-by-cve-2026-40900', 0)#
```

After the server's `SELECT * FROM ( ... ) AS pruva_alias LIMIT 100` wrap is
applied this becomes a perfectly valid multi-statement script: the first
statement is a benign SELECT, the second statement is the attacker's
side-effecting query, and the trailing `#` swallows the remainder of the
wrapper (closing paren, alias, LIMIT). With `allowMultiQueries=true` MySQL
runs both statements and the INSERT/UPDATE/DELETE commits to the application
database.

In the full chain documented by Ox Security (auth bypass CVE-2026-23958 ->
JDBC blocklist bypass CVE-2026-40899 -> this CVE -> Quartz scheduler RCE) the
attacker uses CVE-2026-40899 to plant `allowMultiQueries=true` in the JDBC URL
of a datasource. For a standalone reproduction of CVE-2026-40900 we can
shortcut that step by registering the datasource with `allowMultiQueries=true`
manually (as admin) - the bug under test is the missing single-statement
enforcement in `previewSql`.

### Affected / fixed versions

- Vulnerable: all DataEase 2.x up to and including `v2.10.20`
- Fixed: `v2.10.21`

Reproduce on `v2.10.20`. Verify the fix on `v2.10.21`.

### Where to look

Endpoint: `POST /de2api/datasetData/previewSql` (note the `de2api` prefix)

Java sources of interest (paths from the v2.10.20 checkout):

- `core/core-backend/src/main/java/io/dataease/dataset/server/DatasetDataServer.java`
  - `previewSql(PreviewSqlDTO dto)` controller method, route `@PostMapping("previewSql")`
  - delegates to `datasetDataManage.previewSqlWithLog(dto)`
- `core/core-backend/src/main/java/io/dataease/dataset/manage/DatasetDataManage.java`
  - service code that builds the wrapping `SELECT ... FROM ( <sql> )` and executes via the engine/provider
- Compare:
  ```
  git clone https://github.com/dataease/dataease.git
  cd dataease && git diff v2.10.20 v2.10.21 -- core/core-backend/src/main/java/io/dataease/dataset/
  ```
  The fix introduces single-statement / SELECT-only validation around the
  `previewSql` flow.

Read that diff and confirm the root cause before writing the exploit.

### Environment

- Execution backend: **docker**. DataEase is a Spring Boot Java app and needs a
  MySQL backing database; Docker Compose is the cleanest way to stand both up.
- Recommended layout (`docker-compose.yml`):
  - `mysql:8` service, expose 3306 internally, set
    `MYSQL_ROOT_PASSWORD=Pruva2026!`, create a database named `dataease`
    (application DB) and a database named `bizdb` (the "business" datasource
    we'll register through DataEase to host the stacked SQLi target). Add a
    couple of rows to a `bizdb.users` table for realism.
  - `dataease` service: build from the v2.10.20 source via the project's
    Maven build, or use the official prebuilt image at the matching tag
    (try `registry.cn-qingdao.aliyuncs.com/dataease/dataease:v2.10.20` or
    the standalone image bundled with the official one-click installer).
    Configure it to use the `dataease` MySQL DB. Bind 8100 (Spring port).
- DataEase default admin credentials are `admin / DataEase@123456` and the
  first login forces a password change - script this via the change-password
  API after `/de2api/login`.

### Reproduction goals - MANDATORY: live runtime exploitation

The proof must be the vulnerability triggering inside the **running DataEase
HTTP service**. The following are explicitly NOT acceptable and will be
rejected:

- A Java/Maven harness that instantiates `DatasetDataManage` directly and calls
  `previewSql(...)`.
- Bypassing the application and sending the stacked SQL straight to MySQL via
  the `mysql` CLI or a JDBC harness.
- Static analysis, `grep`, or pure diff reading.

Concrete steps:

1. Bring up DataEase v2.10.20 + MySQL with `docker compose up -d`. Wait for
   `/de2api/login` to respond. Reset the admin password if first-login enforces
   it.
2. As admin, authenticate (`POST /de2api/login`) and capture the bearer/X-DE-TOKEN.
3. As admin, register a MySQL datasource pointing at the `bizdb` schema with
   `extraJdbcParam=allowMultiQueries=true` (or the appropriate field in the
   datasource DTO that ends up appended to the JDBC URL). Verify the
   connection via `/de2api/datasource/validate`.
4. Capture the **pre-exploit** state of a known target table on `bizdb` (e.g.
   `SELECT COUNT(*) FROM bizdb.users` via a legitimate SELECT through
   `previewSql`, or - acceptable here because it's only used as an oracle, not
   as the exploit - via a sidecar `mysql` query).
5. Send the exploit: `POST /de2api/datasetData/previewSql` with body:
   ```json
   {
     "datasourceId": "<id-from-step-3>",
     "sql": "SELECT 1 FROM dual) AS x; INSERT INTO users (id, name) VALUES (999999999, 'pwned-by-cve-2026-40900')#"
   }
   ```
   Save the full HTTP response.
6. Re-query the target table and show the new row exists (the **side-effect
   is the proof** - a single-statement SELECT engine could never have produced
   it).
7. Tear down, redeploy with the `v2.10.21` image / build, replay the exact
   same authenticated request, and show the side-effect does NOT happen (the
   server rejects the multi-statement payload or strips it).
8. If a step is genuinely impossible (e.g. v2.10.20 image not buildable in the
   sandbox), report `BLOCKED` with the specific obstacle - do NOT fall back to
   a harness or to direct-to-MySQL execution.

### Expected artifacts

- `repro/reproduction_steps.sh` - runnable end-to-end: stands up DataEase +
  MySQL via docker compose, performs login, registers the
  allowMultiQueries=true datasource, fires the stacked-SQL HTTP request,
  asserts the side-effect on v2.10.20, then repeats against v2.10.21 and
  asserts the side-effect does NOT occur.
- `repro/rca_report.md` - root cause derived from the `v2.10.20..v2.10.21`
  diff of `DatasetDataServer.java` / `DatasetDataManage.java`.
- `repro/patch_analysis.md` - confirmation of the fix, citing the specific
  validation added in v2.10.21.
- Captured evidence: HTTP request/response transcripts for both versions, and
  the before/after row counts on the target table proving the
  INSERT/UPDATE/DELETE committed on the vulnerable build only.
