{"repro_id":"REPRO-2026-00169","version":7,"title":"DataEase: stacked-query SQL injection via previewSql with allowMultiQueries","repro_type":"security","status":"published","severity":"high","description":"DataEase's \"preview SQL\" endpoint takes user-supplied SQL meant to represent a\ndataset's source query and wraps it inside a subquery, roughly:\n\n```\nSELECT * FROM ( <USER_SQL> ) AS pruva_alias LIMIT 100\n```\n\nThe server does NOT validate that the user input is a single `SELECT`\nstatement. When the underlying MySQL JDBC connection has\n`allowMultiQueries=true` (set on the JDBC URL of the configured datasource), a\ncrafted payload using MySQL's `#` comment terminator can escape the wrapping\nsubquery and execute arbitrary stacked statements. Example payload:\n\n```\nSELECT 1 FROM dual) AS x; INSERT INTO core_msg_type (id, name, pid) VALUES (999999999, 'pwned-by-cve-2026-40900', 0)#\n```\n\nAfter the server's `SELECT * FROM ( ... ) AS pruva_alias LIMIT 100` wrap is\napplied this becomes a perfectly valid multi-statement script: the first\nstatement is a benign SELECT, the second statement is the attacker's\nside-effecting query, and the trailing `#` swallows the remainder of the\nwrapper (closing paren, alias, LIMIT). With `allowMultiQueries=true` MySQL\nruns both statements and the INSERT/UPDATE/DELETE commits to the application\ndatabase.\n\nIn the full chain documented by Ox Security (auth bypass CVE-2026-23958 ->\nJDBC blocklist bypass CVE-2026-40899 -> this CVE -> Quartz scheduler RCE) the\nattacker uses CVE-2026-40899 to plant `allowMultiQueries=true` in the JDBC URL\nof a datasource. For a standalone reproduction of CVE-2026-40900 we can\nshortcut that step by registering the datasource with `allowMultiQueries=true`\nmanually (as admin) - the bug under test is the missing single-statement\nenforcement in `previewSql`.","root_cause":"# RCA Report: CVE-2026-40900\n\n## Summary\n\nCVE-2026-40900 is a stacked-query SQL injection vulnerability in DataEase's `previewSql` endpoint (`POST /de2api/datasetData/previewSql`). The endpoint accepts arbitrary user-supplied SQL and wraps it inside a subquery (`SELECT * FROM ( <USER_SQL> ) AS alias LIMIT 100`) without enforcing that the input is a single SELECT statement. When the underlying MySQL JDBC connection is configured with `allowMultiQueries=true`, an attacker can craft a payload that escapes the wrapping subquery using a closing parenthesis and semicolon, executes a second side-effecting statement (INSERT/UPDATE/DELETE), and uses a MySQL `#` comment to swallow the remainder of the wrapper. This grants full read/write access to the application database.\n\n## Impact\n\n- **Package/component affected**: DataEase (`io.dataease:datasource:type:Mysql` and `dataset:manage:DatasetDataManage`)\n- **Affected versions**: `<= v2.10.20`\n- **Fixed versions**: `v2.10.21`\n- **Risk level**: High (CVSS 3.1: 8.8)\n- **Consequences**: Authenticated attacker can execute arbitrary stacked SQL against the DataEase application database, including INSERT/UPDATE/DELETE on tables such as `core_msg_type` or Quartz scheduler tables, enabling further privilege escalation or RCE as documented in the Ox Security writeup.\n\n## Root Cause\n\nThe vulnerability has two contributing factors:\n\n1. **Missing `allowMultiQueries` in MySQL JDBC parameter blocklist**: In `core/core-backend/src/main/java/io/dataease/datasource/type/Mysql.java` (v2.10.20), the `illegalParameters` list did not include `allowMultiQueries`. This allowed an admin (or attacker who had compromised admin privileges via the preceding CVEs in the chain) to register a MySQL datasource whose JDBC URL contained `allowMultiQueries=true`.\n\n2. **No single-statement validation in `previewSql`**: `DatasetDataManage.previewSql()` wraps the user-provided SQL string in a subquery without parsing or validating that it is a single SELECT statement. When `allowMultiQueries=true`, the MySQL JDBC driver happily executes multiple statements in one call.\n\nThe attacker payload:\n```\nSELECT 1 FROM dual) AS x; INSERT INTO repro_test (id, name) VALUES (999999999, 'pwned')#\n```\nbecomes:\n```\nSELECT * FROM ( SELECT 1 FROM dual) AS x; INSERT INTO repro_test ... # ) AS alias LIMIT 100\n```\nThe `#` comments out `) AS alias LIMIT 100`, leaving two valid statements, both of which MySQL executes.\n\n**Fix commit**: `15611593b3631b5a25528b9cdb2ee517ef27929a` — adds `\"allowMultiQueries\"` to the `illegalParameters` list in `Mysql.java`. When the datasource configuration is validated during save/update, `JdbcUrlSecurityPolicy.validate()` now rejects any MySQL JDBC URL or extra parameters containing `allowMultiQueries`, causing the datasource to be saved with `status=\"Error\"`. `previewSql` refuses to run against datasources with Error status, blocking the exploit path.\n\n## Reproduction Steps\n\nSee `repro/reproduction_steps.sh` for the automated end-to-end reproduction. At a high level:\n\n1. Start DataEase `v2.10.20` + MySQL via Docker Compose.\n2. Log in as `admin` / `DataEase@123456` (RSA-encrypted credentials fetched via `/de2api/dekey`).\n3. Create a MySQL datasource with `extraParams=allowMultiQueries=true`.\n4. Validate the datasource (succeeds on v2.10.20).\n5. Send `POST /de2api/datasetData/previewSql` with a base64-encoded stacked-SQL payload:\n   ```\n   SELECT 1 FROM dual) AS x; INSERT INTO repro_test (id, name) VALUES (999999999, 'pwned-by-cve-2026-40900')#\n   ```\n6. Query MySQL: a new row exists in `repro_test` — proving the INSERT executed.\n7. Tear down, redeploy with `v2.10.21`, repeat the same save request.\n8. On v2.10.21 the datasource is saved but marked `status=\"Error\"` because `allowMultiQueries` is rejected by the updated `illegalParameters` blocklist. `previewSql` refuses to run, and no side-effect occurs.\n\n## Evidence\n\n- `logs/v2.10.20_exploit.json` — HTTP response from `previewSql` on the vulnerable build.\n- `logs/v2.10.20_validate.json` — datasource validation response showing `status=\"Success\"`.\n- `logs/v2.10.21_create_datasource.json` — save response on the fixed build showing `status=\"Error\"`.\n- Console output from the reproduction script shows:\n  - v2.10.20: `Post-exploit repro_test count: 1`\n  - v2.10.21: datasource unusable, `count: 0`\n\n## Recommendations / Next Steps\n\n1. **Upgrade to v2.10.21 or later** — the vendor patch is minimal and targeted.\n2. **Additional defense-in-depth**: add a server-side SQL parser (e.g., JSqlParser or Calcite SQL validation) to enforce that `previewSql` input contains exactly one SELECT statement before wrapping it.\n3. **Audit existing MySQL datasources** for any that have `allowMultiQueries=true` in their configuration and remove or reconfigure them.\n4. **Regression test**: include the stacked-SQL payload in the CI pipeline for `previewSql` to ensure future changes don't re-introduce the bypass.\n\n## Additional Notes\n\n- **Idempotency**: The script was run twice consecutively, both times producing the same results (vulnerable side-effect confirmed on v2.10.20, blocked on v2.10.21).\n- **Limitations**: The reproduction requires running the full DataEase Spring Boot container and MySQL container, so each run takes ~60–90 seconds. The script handles cleanup automatically via `trap cleanup EXIT`.\n- The fix is in the datasource configuration layer (`Mysql.illegalParameters`) rather than in the `previewSql` query-wrapping logic itself. While this blocks the specific `allowMultiQueries` vector, a more robust fix would also validate the SQL structure in `previewSql` to defend against other JDBC-parameter bypasses.\n","cve_id":"CVE-2026-40900","cwe_id":"CWE-89 (SQL Injection)","source_url":"https://github.com/dataease/dataease","package":{"name":"dataease","ecosystem":"github","affected_versions":"<= v2.10.20","fixed_version":"v2.10.21"},"reproduced_at":"2026-05-26T00:25:43.961554+00:00","duration_secs":3509.9739933013916,"tool_calls":582,"turns":510,"handoffs":2,"total_cost_usd":11.11298066000001,"agent_costs":{"repro":8.736447650000008,"support":0.02901206,"vuln_variant":2.3475209500000003},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":8.736447650000008},"support":{"accounts/fireworks/models/kimi-k2p6":0.02901206},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":2.3475209500000003}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-26T00:25:46.259230+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":5670,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12221,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":9150,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":12551,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":4850,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":845,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":7315,"category":"ticket"},{"path":"bundle/repro/my.cnf","filename":"my.cnf","size":94,"category":"other"},{"path":"bundle/repro/init.sql","filename":"init.sql","size":99,"category":"other"},{"path":"bundle/repro/patch_analysis.md","filename":"patch_analysis.md","size":3220,"category":"documentation"},{"path":"bundle/repro/docker-compose.yml","filename":"docker-compose.yml","size":1029,"category":"other"},{"path":"bundle/repro/application.yml","filename":"application.yml","size":612,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1550,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1609,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":4159,"category":"documentation"},{"path":"bundle/vuln_variant/docker-compose.yml","filename":"docker-compose.yml","size":1404,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":3260,"category":"other"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1065,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":4084,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":783,"category":"other"},{"path":"bundle/logs/variant3_v2.10.21_create.json","filename":"variant3_v2.10.21_create.json","size":925,"category":"other"},{"path":"bundle/logs/variant1_v2.10.20_exploit.json","filename":"variant1_v2.10.20_exploit.json","size":869,"category":"other"},{"path":"bundle/logs/variant3_v2.10.21_baseline.json","filename":"variant3_v2.10.21_baseline.json","size":822,"category":"other"},{"path":"bundle/logs/variant2_v2.10.20_exploit.json","filename":"variant2_v2.10.20_exploit.json","size":330,"category":"other"},{"path":"bundle/logs/variant_run.log","filename":"variant_run.log","size":4287,"category":"log"},{"path":"bundle/logs/variant_run7.log","filename":"variant_run7.log","size":2770,"category":"log"},{"path":"bundle/logs/variant3_v2.10.20_exploit.json","filename":"variant3_v2.10.20_exploit.json","size":151,"category":"other"},{"path":"bundle/logs/variant3_v2.10.21_exploit.json","filename":"variant3_v2.10.21_exploit.json","size":161,"category":"other"},{"path":"bundle/logs/variant_run5.log","filename":"variant_run5.log","size":917,"category":"log"},{"path":"bundle/logs/variant_run4.log","filename":"variant_run4.log","size":936,"category":"log"},{"path":"bundle/logs/v2.10.20_validate.json","filename":"v2.10.20_validate.json","size":566,"category":"other"},{"path":"bundle/logs/variant3_v2.10.20_baseline.json","filename":"variant3_v2.10.20_baseline.json","size":822,"category":"other"},{"path":"bundle/logs/variant_run3.log","filename":"variant_run3.log","size":824,"category":"log"},{"path":"bundle/logs/variant2_v2.10.21_exploit.json","filename":"variant2_v2.10.21_exploit.json","size":330,"category":"other"},{"path":"bundle/logs/variant_run2.log","filename":"variant_run2.log","size":825,"category":"log"},{"path":"bundle/logs/v2.10.21_create_datasource.json","filename":"v2.10.21_create_datasource.json","size":928,"category":"other"},{"path":"bundle/logs/variant1_v2.10.20_create.json","filename":"variant1_v2.10.20_create.json","size":944,"category":"other"},{"path":"bundle/logs/variant_v2.10.21_docker.log","filename":"variant_v2.10.21_docker.log","size":576,"category":"log"},{"path":"bundle/logs/v2.10.21_exploit.json","filename":"v2.10.21_exploit.json","size":51,"category":"other"},{"path":"bundle/logs/variant2_v2.10.20_create.json","filename":"variant2_v2.10.20_create.json","size":942,"category":"other"},{"path":"bundle/logs/variant_v2.10.20_docker.log","filename":"variant_v2.10.20_docker.log","size":576,"category":"log"},{"path":"bundle/logs/variant2_v2.10.21_create.json","filename":"variant2_v2.10.21_create.json","size":942,"category":"other"},{"path":"bundle/logs/v2.10.21_validate.json","filename":"v2.10.21_validate.json","size":100,"category":"other"},{"path":"bundle/logs/variant1_v2.10.21_create.json","filename":"variant1_v2.10.21_create.json","size":942,"category":"other"},{"path":"bundle/logs/v2.10.20_exploit.json","filename":"v2.10.20_exploit.json","size":762,"category":"other"},{"path":"bundle/logs/variant3_v2.10.20_create.json","filename":"variant3_v2.10.20_create.json","size":925,"category":"other"},{"path":"bundle/logs/variant_run6.log","filename":"variant_run6.log","size":2746,"category":"log"},{"path":"bundle/logs/variant_run8.log","filename":"variant_run8.log","size":2839,"category":"log"}]}