From 362a84dde94d0829e2b1f8a59a688d37e2c14689 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 29 Apr 2026 01:09:13 +0200 Subject: [PATCH] fix(escapeHtml): cover apostrophe to harden single-quoted attribute use Sina #5505 action item: escapeHtml escaped the four common entities but not the apostrophe. Today every consumer uses double-quoted attributes, but a future renderer change to single quotes would silently open a stored-XSS hole. Cheaper to fix now, with a regression test. Also pin the idempotence-by-composition property: a second call re-escapes the & introduced by the first. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/utils/mention.spec.ts | 11 +++++++++++ frontend/src/lib/utils/mention.ts | 10 ++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/utils/mention.spec.ts b/frontend/src/lib/utils/mention.spec.ts index 4f15bc10..04b659b1 100644 --- a/frontend/src/lib/utils/mention.spec.ts +++ b/frontend/src/lib/utils/mention.spec.ts @@ -24,6 +24,17 @@ describe('escapeHtml', () => { it('escapes ampersand before other entities to avoid double-encoding', () => { expect(escapeHtml('a& { + expect(escapeHtml("d'Artagnan")).toBe('d'Artagnan'); + }); + + it('does not collapse already-encoded entities (re-escapes the &)', () => { + // escapeHtml is idempotent by composition: the second pass re-escapes + // the & that was added by the first. Pin the property so the helper + // can't be "cleverly" optimised to skip it. + expect(escapeHtml('&')).toBe('&amp;'); + }); }); // ─── detectMention ──────────────────────────────────────────────────────────── diff --git a/frontend/src/lib/utils/mention.ts b/frontend/src/lib/utils/mention.ts index c0e0f11b..8b91bc1c 100644 --- a/frontend/src/lib/utils/mention.ts +++ b/frontend/src/lib/utils/mention.ts @@ -45,15 +45,21 @@ export function extractContent( } /** - * Escapes the four HTML-special characters that can break out of text content + * Escapes the five HTML-special characters that can break out of text content * or attribute values. & must be escaped first to avoid double-encoding. + * + * Includes the apostrophe so the helper is safe in single-quoted attribute + * values too — the renderTranscriptionBody anchor template in PR-B2 uses + * double quotes today, but a future template change shouldn't open a + * stored-XSS hole (Sina #5505 action item). */ export function escapeHtml(str: string): string { return str .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') - .replaceAll('"', '"'); + .replaceAll('"', '"') + .replaceAll("'", '''); } /**