fix(timeline): harden curator event precision field
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m51s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 4m35s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
SDD Gate / RTM Check (pull_request) Successful in 13s
SDD Gate / Contract Validate (pull_request) Successful in 22s
SDD Gate / Constitution Impact (pull_request) Successful in 17s

- Validate the submitted precision against the DatePrecision allow-list in
  parseEventForm (falls back to DAY) so an untrusted token can't flow into
  the request body — symmetric with the existing `type` narrowing.
- Parameterize the precision input name via DatePrecisionField's new
  precisionInputName prop; the timeline form now submits `precision` instead
  of the misleading document-domain `metaDatePrecision`. Document form keeps
  the default, so its behaviour is unchanged.
- Capture EventTypeSelect's onchange into EventForm's `type` state so it no
  longer goes stale (the submitted value was already correct via the hidden
  input; this keeps the local state in sync).

Addresses PR #832 review (#781).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-13 23:22:11 +02:00
parent 9f17c4538f
commit cd5649b96e
4 changed files with 51 additions and 6 deletions

View File

@@ -8,6 +8,20 @@ type TimelineEventRequest = components['schemas']['TimelineEventRequest'];
// Prevents open redirect: validate before constructing /persons/{id}. See OWASP CWE-601.
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// Whitelist of accepted precision tokens — mirrors the DatePrecision union. Any
// other submitted value falls back to DAY rather than flowing untrusted into the
// request body (the backend enum is the hard gate; this keeps the two symmetric
// with the `type` narrowing below).
const VALID_PRECISIONS: readonly DatePrecision[] = [
'DAY',
'MONTH',
'SEASON',
'YEAR',
'RANGE',
'APPROX',
'UNKNOWN'
];
/**
* Resolves the context-aware post-save / post-delete redirect target. Returns
* the originating person page only when `originPersonIdRaw` is a strict UUID;
@@ -33,7 +47,9 @@ export interface ParsedEventForm {
export function parseEventForm(formData: FormData): ParsedEventForm {
const rawType = formData.get('type')?.toString();
const type = rawType === 'HISTORICAL' ? 'HISTORICAL' : 'PERSONAL';
const precision = (formData.get('metaDatePrecision')?.toString() as DatePrecision) || 'DAY';
const rawPrecision = formData.get('precision')?.toString() as DatePrecision | undefined;
const precision: DatePrecision =
rawPrecision && VALID_PRECISIONS.includes(rawPrecision) ? rawPrecision : 'DAY';
const endRaw = formData.get('eventDateEnd')?.toString().trim() ?? '';
// Off-RANGE submits an empty string → null so a stale end-date never persists.
const eventDateEnd = precision === 'RANGE' && endRaw ? endRaw : null;