From e3140c4f99c4593139b86afb994178ac2d97765d Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 12 Jun 2026 11:43:38 +0200 Subject: [PATCH] fix(geschichte): null status always resolves to PUBLISHED, fixing CWE-639 A blog writer passing null status previously forwarded null to the repository, returning all stories including other authors' drafts. Now only an explicit DRAFT request (blog writer only) scopes to the caller's own stories. Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/geschichte/GeschichteService.java | 9 +++++++-- .../familienarchiv/geschichte/GeschichteServiceTest.java | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteService.java b/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteService.java index cbc8f6d1..ffc932e7 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/geschichte/GeschichteService.java @@ -117,12 +117,17 @@ public class GeschichteService { * *

Returns a {@link GeschichteSummary} projection — never carries items, preventing * LazyInitializationException on the non-transactional list path. + * + *

Security: {@code null} status always resolves to PUBLISHED — even for blog writers. + * Only an explicit {@code DRAFT} request scopes the query to the caller's own drafts. + * This prevents CWE-639: a blog writer passing {@code null} must not see all authors' drafts. */ public List list(GeschichteStatus status, List personIds, UUID documentId, int limit) { - GeschichteStatus effective = currentUserHasBlogWrite() ? status : GeschichteStatus.PUBLISHED; + boolean isDraftRequest = currentUserHasBlogWrite() && status == GeschichteStatus.DRAFT; + GeschichteStatus effective = isDraftRequest ? GeschichteStatus.DRAFT : GeschichteStatus.PUBLISHED; int safeLimit = limit <= 0 ? DEFAULT_LIMIT : Math.min(limit, MAX_LIMIT); - UUID authorId = effective == GeschichteStatus.DRAFT ? currentUser().getId() : null; + UUID authorId = isDraftRequest ? currentUser().getId() : null; // When personIds is empty, personCount=0 short-circuits the IN() predicate. // Pass a sentinel UUID to avoid invalid empty IN() SQL while the predicate is skipped. diff --git a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceTest.java index 261b5bd9..1d183a3c 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/geschichte/GeschichteServiceTest.java @@ -324,6 +324,7 @@ class GeschichteServiceTest { @DisplayName("security: DRAFT status scopes to current user only") void list_with_DRAFT_status_scopes_to_current_user_not_all_authors() { authenticateAs(writer, Permission.BLOG_WRITE); + when(userService.findByEmail(writer.getEmail())).thenReturn(writer); when(geschichteRepository.findSummaries(any(), any(), any(), anyLong(), any())) .thenReturn(List.of());