test(PersonMentionEditor): assert no HTML injection via mention displayName
Adds a CWE-79 regression test: a sidecar entry whose displayName contains an <img onerror=alert(1)> payload must round-trip through deserialize and the Tiptap renderHTML without producing a real <img> element in the editor DOM. Locks down the "renderHTML's third tuple entry is a text node, never parsed as HTML" invariant so a future "use innerHTML for performance" refactor cannot silently regress. Nora #5618 detection-gap concern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -316,6 +316,40 @@ describe('PersonMentionEditor — disabled state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Security — XSS in displayName (CWE-79) ──────────────────────────────────
|
||||
|
||||
describe('PersonMentionEditor — XSS resistance', () => {
|
||||
it('renders a malicious displayName as text, not as HTML elements', async () => {
|
||||
// A historical sidecar entry whose displayName contains an HTML payload
|
||||
// that would execute if interpolated as raw HTML. Tiptap's renderHTML
|
||||
// returns the @-prefixed string as the third tuple entry, which
|
||||
// ProseMirror's DOMSerializer treats as a Text node — escaping it.
|
||||
const maliciousMention: PersonMention = {
|
||||
personId: '00000000-0000-0000-0000-000000000001',
|
||||
displayName: '<img src=x onerror=alert(1)>'
|
||||
};
|
||||
|
||||
renderHost({
|
||||
value: '@<img src=x onerror=alert(1)>',
|
||||
mentionedPersons: [maliciousMention]
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const textbox = document.querySelector('[role="textbox"]') as HTMLElement | null;
|
||||
expect(textbox).not.toBeNull();
|
||||
// No element from the malicious payload should have appeared as a real
|
||||
// DOM node. (Tiptap inserts its own ProseMirror-separator <img> in empty
|
||||
// paragraphs — that is internal markup and never carries user attrs;
|
||||
// guard against the injection by checking the user-controlled attrs.)
|
||||
expect(textbox!.querySelector('img[onerror]')).toBeNull();
|
||||
expect(textbox!.querySelector('img[src="x"]')).toBeNull();
|
||||
expect(textbox!.querySelector('script')).toBeNull();
|
||||
// The payload should appear as visible text content instead.
|
||||
expect(textbox!.textContent ?? '').toContain('<img src=x onerror=alert(1)>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Touch target (WCAG 2.2 AA) ──────────────────────────────────────────────
|
||||
|
||||
describe('PersonMentionEditor — touch target', () => {
|
||||
|
||||
Reference in New Issue
Block a user