🔒️(frontend) sanitize color during collaboration

To improve security we sanitize the color used
for collaboration presence to ensure it's a valid
hex color. If the color is not valid, we generate
a random color instead. This prevents potential
issues with invalid color values being used in the UI.
This commit is contained in:
Anthony LC
2026-05-05 15:21:16 +02:00
parent 85128c7b11
commit 0f527a789a
4 changed files with 47 additions and 3 deletions
+4
View File
@@ -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
@@ -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);
});
});
@@ -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);
@@ -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;