refactor(person-mention): brand renderer return types as SafeHtml
Markus, Felix, and Nora independently flagged the {@html …} boundary as a
distributed-knowledge security risk: today renderBody and renderTranscriptionBody
return string, so the next refactor that does {@html block.text} (instead of
{@html renderBlockHtml(block)}) is one typo away from a stored-XSS regression.
Introduce a SafeHtml brand type (string with a phantom __brand) returned by
both renderers and by renderBlockHtml in TranscriptionReadView. Compile-time
enforcement of the escape invariant — costs zero runtime, makes the contract
auditable in one file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import type { TranscriptionBlockData } from '$lib/types';
|
||||
import type { components } from '$lib/generated/api';
|
||||
import { splitByMarkers } from '$lib/utils/transcriptionMarkers';
|
||||
import { renderTranscriptionBody } from '$lib/utils/mention';
|
||||
import { renderTranscriptionBody, type SafeHtml } from '$lib/utils/mention';
|
||||
import PersonHoverCard from './PersonHoverCard.svelte';
|
||||
import type { HoverData, LoadState } from '$lib/types/personHoverCard';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -42,15 +42,18 @@ const CARD_GAP = 6;
|
||||
// as <em data-marker> tags; text segments run through HTML-escaping + mention
|
||||
// substitution. The two are concatenated to preserve marker boundaries — markers
|
||||
// never end up nested inside an anchor (Felix #5324 B19b).
|
||||
function renderBlockHtml(block: TranscriptionBlockData): string {
|
||||
function renderBlockHtml(block: TranscriptionBlockData): SafeHtml {
|
||||
return splitByMarkers(block.text)
|
||||
.map((segment) => {
|
||||
if (segment.type === 'marker') {
|
||||
return `<em data-marker class="text-ink-2 italic">${segment.text}</em>`;
|
||||
// splitByMarkers only emits the literal markers [unleserlich] and [...],
|
||||
// no user input — safe to embed directly. Wrap in SafeHtml to satisfy
|
||||
// the brand contract.
|
||||
return `<em data-marker class="text-ink-2 italic">${segment.text}</em>` as SafeHtml;
|
||||
}
|
||||
return renderTranscriptionBody(segment.text, block.mentionedPersons ?? []);
|
||||
})
|
||||
.join('');
|
||||
.join('') as SafeHtml;
|
||||
}
|
||||
|
||||
function fetchHoverData(personId: string): Promise<HoverData | null> {
|
||||
|
||||
Reference in New Issue
Block a user