diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index 245895ada..3a6409919 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -39,7 +39,8 @@ const LINK_REGEX = /^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?)$/i; const SANITIZATION_CONFIG = { - linkRegex: LINK_REGEX, + linkRegex: + /^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?|https?:\/\/\S+)$/i, }; function App() { diff --git a/apps/example-web/src/components/TextRenderer.tsx b/apps/example-web/src/components/TextRenderer.tsx index 0c2b8fd9c..2598ecb39 100644 --- a/apps/example-web/src/components/TextRenderer.tsx +++ b/apps/example-web/src/components/TextRenderer.tsx @@ -12,7 +12,7 @@ import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle'; import { EnrichedTextActions } from './EnrichedTextActions'; const LINK_REGEX = - /^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?)$/i; + /^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?|https?:\/\/\S+)$/i; const SANITIZATION_CONFIG = { linkRegex: LINK_REGEX, diff --git a/src/web/EnrichedText.css b/src/web/EnrichedText.css index f9175ff57..90018746e 100644 --- a/src/web/EnrichedText.css +++ b/src/web/EnrichedText.css @@ -366,7 +366,9 @@ vertical-align: text-bottom; } -.et-view img.error { +.et-view img.error, +.et-view img:not([src]), +.et-view img[src=""] { content: linear-gradient(transparent, transparent); background-color: currentColor; -webkit-mask: var(--et-broken-image-glyph) no-repeat center / contain; diff --git a/src/web/__tests__/sanitization.test.ts b/src/web/__tests__/sanitization.test.ts index 9cdb3c800..759f00bfc 100644 --- a/src/web/__tests__/sanitization.test.ts +++ b/src/web/__tests__/sanitization.test.ts @@ -99,6 +99,57 @@ describe('sanitizeHtmlMention', () => { }); }); +describe('sanitizeHtml ', () => { + const urlOnlyRegex = /^(?:enriched:\/\/\S+|https?:\/\/\S+)$/i; + + it('keeps src, width, and height with the default config', () => { + const out = sanitizeHtml( + '' + ); + expect(out).toContain('src="https://example.com/a.png"'); + expect(out).toContain('width="80"'); + expect(out).toContain('height="60"'); + }); + + it('keeps width and height even when a URL-only linkRegex is supplied', () => { + const out = sanitizeHtml( + 'cat', + { linkRegex: urlOnlyRegex } + ); + expect(out).toContain('width="80"'); + expect(out).toContain('height="60"'); + expect(out).toContain('src="https://example.com/a.png"'); + expect(out).toContain('alt="cat"'); + }); + + it('still validates the img src protocol against the custom linkRegex', () => { + const out = sanitizeHtml( + '', + { linkRegex: urlOnlyRegex } + ); + expect(out).not.toContain('ftp://'); + expect(out).toContain('width="80"'); + expect(out).toContain('height="60"'); + }); + + it('strips a javascript: src', () => { + const out = sanitizeHtml( + '', + { linkRegex: urlOnlyRegex } + ); + // eslint-disable-next-line no-script-url + expect(out).not.toContain('javascript:'); + }); + + it('strips event handlers from img', () => { + const out = sanitizeHtml( + '' + ); + expect(out).not.toContain('onerror'); + expect(out).toContain('width="80"'); + }); +}); + describe('sanitizeLinkAttributes', () => { it('strips javascript: URLs from links', () => { const out = sanitizeHtml('x'); diff --git a/src/web/sanitization/htmlSanitizer.ts b/src/web/sanitization/htmlSanitizer.ts index 6b8acf442..cc866f0d3 100644 --- a/src/web/sanitization/htmlSanitizer.ts +++ b/src/web/sanitization/htmlSanitizer.ts @@ -3,6 +3,11 @@ import type { SanitizationConfig } from '../../types'; const MENTION_ATTRS = ['text', 'indicator']; +// Non-URL attributes we emit. They must be listed as "URI safe" because +// DOMPurify validates every attribute value that isn't in its built-in +// URI_SAFE_ATTRIBUTES set against ALLOWED_URI_REGEXP. +const IMG_DIMENSION_ATTRS = ['width', 'height']; + // Attributes DOMPurify keeps by default and are commonly used, so we don't emit an unnecessary warning const COMMONLY_ALLOWED_ATTRS = ['id', 'class', 'style']; @@ -10,7 +15,7 @@ export function sanitizeHtml(html: string, config?: SanitizationConfig) { return DOMPurify.sanitize(html, { ADD_TAGS: ['mention', 'codeblock'], ADD_ATTR: MENTION_ATTRS, - ADD_URI_SAFE_ATTR: MENTION_ATTRS, + ADD_URI_SAFE_ATTR: [...MENTION_ATTRS, ...IMG_DIMENSION_ATTRS], // if not supplied, fall back to DOMPurify's built-in default. ...(config?.linkRegex ? { ALLOWED_URI_REGEXP: config.linkRegex } : {}), });