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) ──────────────────────────────────────────────
|
// ─── Touch target (WCAG 2.2 AA) ──────────────────────────────────────────────
|
||||||
|
|
||||||
describe('PersonMentionEditor — touch target', () => {
|
describe('PersonMentionEditor — touch target', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user