Skip to content

Commit 737b5fe

Browse files
committed
fix: img attributes sanitization
1 parent d389e5f commit 737b5fe

5 files changed

Lines changed: 62 additions & 5 deletions

File tree

apps/example-web/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const LINK_REGEX =
3939
/^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?)$/i;
4040

4141
const SANITIZATION_CONFIG = {
42-
linkRegex: LINK_REGEX,
42+
linkRegex: /^(?:enriched:\/\/\S+|https?:\/\/\S+)$/i,
4343
};
4444

4545
function App() {

apps/example-web/src/components/TextRenderer.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import {
1111
import { WEB_DEFAULT_HTML_STYLE } from '../defaultHtmlStyle';
1212
import { EnrichedTextActions } from './EnrichedTextActions';
1313

14-
const LINK_REGEX =
15-
/^(?:enriched:\/\/\S+|(?:https?:\/\/)?(?:www\.)?swmansion\.com(?:\/\S*)?)$/i;
14+
const LINK_REGEX = /^(?:enriched:\/\/\S+|https?:\/\/\S+)$/i;
1615

1716
const SANITIZATION_CONFIG = {
1817
linkRegex: LINK_REGEX,

src/web/EnrichedText.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,9 @@
366366
vertical-align: text-bottom;
367367
}
368368

369-
.et-view img.error {
369+
.et-view img.error,
370+
.et-view img:not([src]),
371+
.et-view img[src=""] {
370372
content: linear-gradient(transparent, transparent);
371373
background-color: currentColor;
372374
-webkit-mask: var(--et-broken-image-glyph) no-repeat center / contain;

src/web/__tests__/sanitization.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,57 @@ describe('sanitizeHtmlMention', () => {
9999
});
100100
});
101101

102+
describe('sanitizeHtml <img>', () => {
103+
const urlOnlyRegex = /^(?:enriched:\/\/\S+|https?:\/\/\S+)$/i;
104+
105+
it('keeps src, width, and height with the default config', () => {
106+
const out = sanitizeHtml(
107+
'<img src="https://example.com/a.png" width="80" height="60">'
108+
);
109+
expect(out).toContain('src="https://example.com/a.png"');
110+
expect(out).toContain('width="80"');
111+
expect(out).toContain('height="60"');
112+
});
113+
114+
it('keeps width and height even when a URL-only linkRegex is supplied', () => {
115+
const out = sanitizeHtml(
116+
'<img src="https://example.com/a.png" width="80" height="60" alt="cat">',
117+
{ linkRegex: urlOnlyRegex }
118+
);
119+
expect(out).toContain('width="80"');
120+
expect(out).toContain('height="60"');
121+
expect(out).toContain('src="https://example.com/a.png"');
122+
expect(out).toContain('alt="cat"');
123+
});
124+
125+
it('still validates the img src protocol against the custom linkRegex', () => {
126+
const out = sanitizeHtml(
127+
'<img src="ftp://example.com/a.png" width="80" height="60">',
128+
{ linkRegex: urlOnlyRegex }
129+
);
130+
expect(out).not.toContain('ftp://');
131+
expect(out).toContain('width="80"');
132+
expect(out).toContain('height="60"');
133+
});
134+
135+
it('strips a javascript: src', () => {
136+
const out = sanitizeHtml(
137+
'<img src="javascript:alert(1)" width="80" height="60">',
138+
{ linkRegex: urlOnlyRegex }
139+
);
140+
// eslint-disable-next-line no-script-url
141+
expect(out).not.toContain('javascript:');
142+
});
143+
144+
it('strips event handlers from img', () => {
145+
const out = sanitizeHtml(
146+
'<img src="https://example.com/a.png" onerror="alert(1)" width="80">'
147+
);
148+
expect(out).not.toContain('onerror');
149+
expect(out).toContain('width="80"');
150+
});
151+
});
152+
102153
describe('sanitizeLinkAttributes', () => {
103154
it('strips javascript: URLs from links', () => {
104155
const out = sanitizeHtml('<a href="javascript:alert(1)">x</a>');

src/web/sanitization/htmlSanitizer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@ import type { SanitizationConfig } from '../../types';
33

44
const MENTION_ATTRS = ['text', 'indicator'];
55

6+
// Non-URL <img> attributes we emit. They must be listed as "URI safe" because
7+
// DOMPurify validates every attribute value that isn't in its built-in
8+
// URI_SAFE_ATTRIBUTES set against ALLOWED_URI_REGEXP.
9+
const IMG_DIMENSION_ATTRS = ['width', 'height'];
10+
611
// Attributes DOMPurify keeps by default and are commonly used, so we don't emit an unnecessary warning
712
const COMMONLY_ALLOWED_ATTRS = ['id', 'class', 'style'];
813

914
export function sanitizeHtml(html: string, config?: SanitizationConfig) {
1015
return DOMPurify.sanitize(html, {
1116
ADD_TAGS: ['mention', 'codeblock'],
1217
ADD_ATTR: MENTION_ATTRS,
13-
ADD_URI_SAFE_ATTR: MENTION_ATTRS,
18+
ADD_URI_SAFE_ATTR: [...MENTION_ATTRS, ...IMG_DIMENSION_ATTRS],
1419
// if not supplied, fall back to DOMPurify's built-in default.
1520
...(config?.linkRegex ? { ALLOWED_URI_REGEXP: config.linkRegex } : {}),
1621
});

0 commit comments

Comments
 (0)