diff --git a/CHANGELOG.md b/CHANGELOG.md index c3f130b65..9b3c3fea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to - 🐛(frontend) Block menu doesn't stay open on 1st line #2229 - 🐛(frontend) The "+" on the first line of a new doc doesn't work #2229 +### Security + +- 🔒️(frontend) sanitize color during collaboration#2270 + ## [v5.0.0] - 2026-04-08 diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/utils.test.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/utils.test.ts new file mode 100644 index 000000000..39fdeb6ee --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/__tests__/utils.test.ts @@ -0,0 +1,33 @@ +import { sanitizeColor } from '../utils'; + +const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/; + +describe('sanitizeColor', () => { + it('accepts valid 6-digit hex colors', () => { + expect(sanitizeColor('#1a2b3c')).toBe('#1a2b3c'); + expect(sanitizeColor('#AABBCC')).toBe('#AABBCC'); + expect(sanitizeColor('#000000')).toBe('#000000'); + expect(sanitizeColor('#ffffff')).toBe('#ffffff'); + }); + + it('rejects 3-digit hex colors and returns a valid random hex color', () => { + expect(sanitizeColor('#abc')).toMatch(HEX_COLOR_RE); + }); + + it('rejects named colors and returns a valid random hex color', () => { + expect(sanitizeColor('red')).toMatch(HEX_COLOR_RE); + expect(sanitizeColor('blue')).toMatch(HEX_COLOR_RE); + }); + + it('rejects CSS injection attempts and returns a valid random hex color', () => { + expect(sanitizeColor('red; behavior: expression(alert(1))')).toMatch( + HEX_COLOR_RE, + ); + expect(sanitizeColor('#fff; color: red')).toMatch(HEX_COLOR_RE); + expect(sanitizeColor('javascript:alert(1)')).toMatch(HEX_COLOR_RE); + }); + + it('rejects empty string and returns a valid random hex color', () => { + expect(sanitizeColor('')).toMatch(HEX_COLOR_RE); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx index f543526ea..6faf7651f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx @@ -36,7 +36,7 @@ import { import { useEditorStore } from '../stores'; import { DocsEditorStyle } from '../styles'; import { DocsBlockNoteEditor } from '../types'; -import { randomColor } from '../utils'; +import { randomColor, sanitizeColor } from '../utils'; import BlockNoteAI from './AI'; import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu'; @@ -152,12 +152,13 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { */ renderCursor: (user: { color: string; name: string }) => { const cursorElement = document.createElement('span'); + const safeColor = sanitizeColor(user.color); cursorElement.classList.add('collaboration-cursor-custom__base'); const caretElement = document.createElement('span'); caretElement.classList.add('collaboration-cursor-custom__caret'); caretElement.setAttribute('spellcheck', `false`); - caretElement.setAttribute('style', `background-color: ${user.color}`); + caretElement.setAttribute('style', `background-color: ${safeColor}`); if (showCursorLabels === 'always') { cursorElement.setAttribute('data-active', ''); @@ -169,7 +170,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => { labelElement.setAttribute('spellcheck', `false`); labelElement.setAttribute( 'style', - `background-color: ${user.color};border: 1px solid ${user.color};`, + `background-color: ${safeColor};border: 1px solid ${safeColor};`, ); labelElement.insertBefore(document.createTextNode(user.name), null); diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts index 325a2304e..e3e5e92b9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-editor/utils.ts @@ -1,3 +1,9 @@ +const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; + +export const sanitizeColor = (color: string): string => { + return HEX_COLOR_REGEX.test(color) ? color : randomColor(); +}; + export const randomColor = () => { const randomInt = (min: number, max: number) => { return Math.floor(Math.random() * (max - min + 1)) + min;