Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sanitize-image-element-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/gazzodown': patch
'@rocket.chat/meteor': patch
---

Sanitizes image URLs in rendered messages to block `javascript:`, `data:`, and `vbscript:` schemes — matching the protection already applied to markdown links. Defense-in-depth against XSS via crafted markdown like `![label](javascript:...)`.
53 changes: 53 additions & 0 deletions packages/gazzodown/src/Markup.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,56 @@ it('renders plain text instead of ASCII emojis based on useEmojis preference', (

expect(screen.getByText('Hey! :smile: :)')).toBeInTheDocument();
});

describe('ImageElement sanitization', () => {
it.each([['javascript:alert(1)'], ['JAVASCRIPT:alert(1)'], ['data:text/html,<script>alert(1)</script>'], ['vbscript:msgbox(1)']])(
'replaces dangerous %s scheme with "#" in image href and src',
(dangerousSrc) => {
render(
<Markup
tokens={[
{
type: 'PARAGRAPH',
value: [
{
type: 'IMAGE',
value: {
src: { type: 'PLAIN_TEXT', value: dangerousSrc },
label: { type: 'PLAIN_TEXT', value: 'click me' },
},
},
],
},
]}
/>,
);

expect(screen.getByRole('link')).toHaveAttribute('href', '#');
expect(screen.getByRole('img')).toHaveAttribute('src', '#');
},
);

it('preserves safe http(s) image URLs untouched', () => {
render(
<Markup
tokens={[
{
type: 'PARAGRAPH',
value: [
{
type: 'IMAGE',
value: {
src: { type: 'PLAIN_TEXT', value: 'https://rocket.chat/logo.svg' },
label: { type: 'PLAIN_TEXT', value: 'logo' },
},
},
],
},
]}
/>,
);

expect(screen.getByRole('link')).toHaveAttribute('href', 'https://rocket.chat/logo.svg');
expect(screen.getByRole('img')).toHaveAttribute('src', 'https://rocket.chat/logo.svg');
});
});
7 changes: 5 additions & 2 deletions packages/gazzodown/src/elements/ImageElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type * as MessageParser from '@rocket.chat/message-parser';
import type { ReactElement } from 'react';
import { useMemo } from 'react';

import { sanitizeUrl } from './sanitizeUrl';

const flattenMarkup = (
markup:
| MessageParser.Timestamp
Expand Down Expand Up @@ -47,10 +49,11 @@ type ImageElementProps = {

const ImageElement = ({ src, alt }: ImageElementProps): ReactElement => {
const plainAlt = useMemo(() => flattenMarkup(alt), [alt]);
const safeSrc = sanitizeUrl(src);
Comment thread
julio-rocketchat marked this conversation as resolved.

return (
<a href={src} target='_blank' rel='noopener noreferrer' title={plainAlt}>
<img src={src} data-title={src} alt={plainAlt} style={style} />
<a href={safeSrc} target='_blank' rel='noopener noreferrer' title={plainAlt}>
<img src={safeSrc} data-title={safeSrc} alt={plainAlt} style={style} />
</a>
);
};
Expand Down
Loading