GET /api/persons leaks PersonSummaryDTO.notes to typeahead clients (CWE-200) #374

Closed
opened 2026-04-29 16:22:31 +02:00 by marcel · 0 comments
Owner

Context

Surfaced during PR #373 review by Nora Steiner (@nullx) — comment #5618, concern #3.

PR #373 introduces a Tiptap-based person mention typeahead in PersonMentionEditor.svelte. The component fetches GET /api/persons?q=<query> on every keystroke after @ and renders the response in a dropdown for the transcriber.

Finding

PersonController.getPersons() returns List<PersonSummaryDTO>. The DTO interface (backend/src/main/java/org/raddatz/familienarchiv/dto/PersonSummaryDTO.java) currently exposes:

public interface PersonSummaryDTO {
    UUID getId();
    String getTitle();
    String getFirstName();
    String getLastName();
    String getPersonType();
    String getAlias();
    Integer getBirthYear();
    Integer getDeathYear();
    String getNotes();          // ← private biographical free text
    boolean isFamilyMember();
    long getDocumentCount();
    default String getDisplayName() { ... }
}

Person.notes is described in the entity itself as "Optional: Free-text biographical notes" — i.e. it is intended to hold sensitive biographical content, possibly including living-relative information, medical/personal observations, or unpublished family history.

Every transcriber typing @ in the editor causes notes for every matched person to be transmitted to the browser. A determined user can scrape all notes simply by typing single letters and inspecting the network panel — without needing direct access to the persons admin page.

This is a CWE-200 (Information Exposure) issue: the API over-exposes data the typeahead does not need.

Acceptance Criteria

  • AC-1: Carve out a typeahead-safe DTO (proposed: PersonTypeaheadDTO) returning only fields the dropdown actually renders: id, title, firstName, lastName, personType, birthYear, deathYear, displayName (derived). NO notes, NO alias, NO documentCount, NO isFamilyMember unless the typeahead UI uses them.
  • AC-2: Add a typed endpoint variant or a query-param flag (/api/persons?q=…&shape=typeahead) that returns the new DTO. The list page (/persons) keeps using the current PersonSummaryDTO.
  • AC-3: PersonMentionEditor.svelte fetch URL switches to the typeahead variant; types regenerated.
  • AC-4: Backend test: PersonControllerTest asserts the typeahead response does NOT include notes (lock down the API surface — Nora explicitly recommended this).
  • AC-5: Frontend type narrowing: replace the Person cast in the dropdown with the new typeahead-safe DTO type so a future field-leak in the wide DTO doesn't silently re-leak.

Non-functional

  • Security: this issue exists because the dropdown is the lowest-privilege caller of the persons list. The fix is "make the API caller-shape-aware" rather than "add field-level security to the wide DTO" — keeping the privileged DTO available for the persons-admin page.
  • Performance: smaller payload per keystroke. Notes can be paragraphs of text; trimming them improves typeahead responsiveness.

Out of scope

  • The full persons list page (/persons) and detail page may continue to expose notes to readers with READ_ALL permission — that is the intended privilege level for that route.
  • Audit logging on persons reads: tracked separately if/when needed.

Source

Nora Steiner, PR #373 review, 2026-04-29.

## Context Surfaced during PR #373 review by Nora Steiner (@nullx) — comment [#5618](http://heim-nas:3005/marcel/familienarchiv/pulls/373#issuecomment-5618), concern #3. PR #373 introduces a Tiptap-based person mention typeahead in `PersonMentionEditor.svelte`. The component fetches `GET /api/persons?q=<query>` on every keystroke after `@` and renders the response in a dropdown for the transcriber. ## Finding `PersonController.getPersons()` returns `List<PersonSummaryDTO>`. The DTO interface (`backend/src/main/java/org/raddatz/familienarchiv/dto/PersonSummaryDTO.java`) currently exposes: ```java public interface PersonSummaryDTO { UUID getId(); String getTitle(); String getFirstName(); String getLastName(); String getPersonType(); String getAlias(); Integer getBirthYear(); Integer getDeathYear(); String getNotes(); // ← private biographical free text boolean isFamilyMember(); long getDocumentCount(); default String getDisplayName() { ... } } ``` `Person.notes` is described in the entity itself as "Optional: Free-text biographical notes" — i.e. it is intended to hold sensitive biographical content, possibly including living-relative information, medical/personal observations, or unpublished family history. Every transcriber typing `@` in the editor causes `notes` for every matched person to be transmitted to the browser. A determined user can scrape all notes simply by typing single letters and inspecting the network panel — without needing direct access to the persons admin page. This is a CWE-200 (Information Exposure) issue: the API over-exposes data the typeahead does not need. ## Acceptance Criteria - [ ] **AC-1**: Carve out a typeahead-safe DTO (proposed: `PersonTypeaheadDTO`) returning only fields the dropdown actually renders: `id`, `title`, `firstName`, `lastName`, `personType`, `birthYear`, `deathYear`, `displayName` (derived). NO `notes`, NO `alias`, NO `documentCount`, NO `isFamilyMember` unless the typeahead UI uses them. - [ ] **AC-2**: Add a typed endpoint variant or a query-param flag (`/api/persons?q=…&shape=typeahead`) that returns the new DTO. The list page (`/persons`) keeps using the current `PersonSummaryDTO`. - [ ] **AC-3**: `PersonMentionEditor.svelte` fetch URL switches to the typeahead variant; types regenerated. - [ ] **AC-4**: Backend test: `PersonControllerTest` asserts the typeahead response does NOT include `notes` (lock down the API surface — Nora explicitly recommended this). - [ ] **AC-5**: Frontend type narrowing: replace the `Person` cast in the dropdown with the new typeahead-safe DTO type so a future field-leak in the wide DTO doesn't silently re-leak. ## Non-functional - **Security**: this issue exists because the dropdown is the lowest-privilege caller of the persons list. The fix is "make the API caller-shape-aware" rather than "add field-level security to the wide DTO" — keeping the privileged DTO available for the persons-admin page. - **Performance**: smaller payload per keystroke. Notes can be paragraphs of text; trimming them improves typeahead responsiveness. ## Out of scope - The full persons list page (`/persons`) and detail page may continue to expose `notes` to readers with `READ_ALL` permission — that is the intended privilege level for that route. - Audit logging on persons reads: tracked separately if/when needed. ## Source Nora Steiner, PR #373 review, 2026-04-29.
marcel added the P1-highbugsecurity labels 2026-04-29 16:22:37 +02:00
Sign in to join this conversation.
No Label P1-high bug security
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#374