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(
+ '
',
+ { 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 } : {}),
});