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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-29 01:09:13 +02:00
parent 49db82e1bd
commit 362a84dde9
2 changed files with 19 additions and 2 deletions

View File

@@ -24,6 +24,17 @@ describe('escapeHtml', () => {
it('escapes ampersand before other entities to avoid double-encoding', () => {
expect(escapeHtml('a&<b')).toBe('a&amp;&lt;b');
});
it('escapes apostrophe to &#39;', () => {
expect(escapeHtml("d'Artagnan")).toBe('d&#39;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('&amp;')).toBe('&amp;amp;');
});
});
// ─── detectMention ────────────────────────────────────────────────────────────

View File

@@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
/**