fix(security): list items need write access to their list (FLAW-27)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s

Creating, checking, editing, deleting, reordering and clearing list
items did not check the list at all: any logged-in user with a list or
item ID could change it, viewers of shared lists as well. All item
endpoints and converting a thought into a list item now require owner
or editor rights and that the item belongs to the list (404 without
access, 403 for viewers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 14:28:39 +01:00
1 parent 11c14fa9ff
commit 3900c6104e
5 files changed
+125

No files matched your search

+35
View File
@@ -2,12 +2,14 @@ package api
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/sandrodach/mindsift/internal/auth"
"github.com/sandrodach/mindsift/internal/domain"
"github.com/sandrodach/mindsift/internal/storage"
)
// --- REALTIME LISTS & SHARING ---
@@ -138,6 +140,9 @@ func (s *Server) handleDeleteListShare(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleCreateListItem(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
listID := chi.URLParam(r, "id")
if !s.listItemsWriteAllowed(w, r, "") {
return
}
var req struct {
Content string `json:"content"`
Category string `json:"category"`
@@ -162,6 +167,9 @@ func (s *Server) handleToggleListItem(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
listID := chi.URLParam(r, "id")
itemID := chi.URLParam(r, "itemId")
if !s.listItemsWriteAllowed(w, r, itemID) {
return
}
var req struct {
IsCompleted bool `json:"isCompleted"`
@@ -190,6 +198,9 @@ func (s *Server) handleToggleListItem(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleUpdateListItem(w http.ResponseWriter, r *http.Request) {
listID := chi.URLParam(r, "id")
itemID := chi.URLParam(r, "itemId")
if !s.listItemsWriteAllowed(w, r, itemID) {
return
}
var req struct {
Content *string `json:"content"`
@@ -214,6 +225,9 @@ func (s *Server) handleUpdateListItem(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleDeleteListItem(w http.ResponseWriter, r *http.Request) {
listID := chi.URLParam(r, "id")
itemID := chi.URLParam(r, "itemId")
if !s.listItemsWriteAllowed(w, r, itemID) {
return
}
_, err := s.store.DeleteListItem(itemID)
if err != nil {
@@ -230,6 +244,9 @@ func (s *Server) handleDeleteListItem(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleClearCompletedItems(w http.ResponseWriter, r *http.Request) {
listID := chi.URLParam(r, "id")
if !s.listItemsWriteAllowed(w, r, "") {
return
}
removed, err := s.store.ClearCompletedListItems(listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
@@ -245,6 +262,9 @@ func (s *Server) handleClearCompletedItems(w http.ResponseWriter, r *http.Reques
func (s *Server) handleReorderListItems(w http.ResponseWriter, r *http.Request) {
listID := chi.URLParam(r, "id")
if !s.listItemsWriteAllowed(w, r, "") {
return
}
var req struct {
Items []domain.ListItemReorderItem `json:"items"`
}
@@ -264,3 +284,18 @@ func (s *Server) handleReorderListItems(w http.ResponseWriter, r *http.Request)
})
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
}
// listItemsWriteAllowed answers 404 (no access, or item not in this list) or 403 (viewer) and returns
// false; every request changing list items goes through it.
func (s *Server) listItemsWriteAllowed(w http.ResponseWriter, r *http.Request, itemID string) bool {
err := s.store.EnsureListItemsWritable(auth.GetUserID(r.Context()), chi.URLParam(r, "id"), itemID)
switch {
case err == nil:
return true
case errors.Is(err, storage.ErrListReadOnly):
writeError(w, http.StatusForbidden, err.Error())
default:
writeError(w, http.StatusNotFound, err.Error())
}
return false
}