Cross-Site-Scripting attack on `<RichTextField>` (GHSA-5jcr-82fh-339v)
Aliases: CVE-2023-25572
Published: 2023-02-14T00:32:21Z
Severity: type: CVSS_V3
score: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
Affected: [{'package': {'ecosystem': 'npm', 'name': 'react-admin'}, 'ranges': [{'type': 'ECOSYSTEM', 'events': [{'introduced': '0'}, {'fixed': '3.19.12'}]}]}, {'package': {'ecosystem': 'npm', 'name': 'react-admin'}, 'ranges': [{'type': 'ECOSYSTEM', 'events': [{'introduced': '4.0.0'}, {'fixed': '4.7.6'}]}]}, {'package': {'ecosystem': 'npm', 'name': 'ra-ui-materialui'}, 'ranges': [{'type': 'ECOSYSTEM', 'events': [{'introduced': '4.0.0'}, {'fixed': '4.7.6'}]}]}, {'package': {'ecosystem': 'npm', 'name': 'ra-ui-materialui'}, 'ranges': [{'type': 'ECOSYSTEM', 'events': [{'introduced': '0'}, {'fixed': '3.19.12'}]}]}]

### Impact

All React applications built with react-admin and using the `<RichTextField>` are affected. 

`<RichTextField>` outputs the field value using `dangerouslySetInnerHTML` without client-side sanitization. If the data isn't sanitized server-side, this opens a possible Cross-Site-Scripting (XSS) attack. 

Proof of concept:

```jsx
import { RichTextField } from 'react-admin';

const record = {
    id: 1,
    body: `
<p>
<strong>War and Peace</strong> is a novel by the Russian author
<a href="https://en.wikipedia.org/wiki/Leo_Tolstoy" onclick="document.getElementById('stolendata').value='credentials';">Leo Tolstoy</a>,
published serially, then in its entirety in 1869.
</p>
<p onmouseover="document.getElementById('stolendata').value='credentials';">
It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature.
</p>
<img src="x" onerror="document.getElementById('stolendata').value='credentials';" />
`,
};

const VulnerableRichTextField = () => (
    <>
        <RichTextField record={record} source="body" />
        <hr />
        <h4>Stolen data:</h4>
        <input id="stolendata" defaultValue="none" />
    </>
);
```

### Patches

Versions 3.19.12 and 4.7.6 now use `DOMPurify` to escape the HTML before outputting it with React and `dangerouslySetInnerHTML`

### Workarounds

You don't need to upgrade if you already sanitize HTML data server-side. 

Otherwise, you'll have to replace the `<RichTextField>` by a custom field doing sanitization by hand:

```tsx
// react-admin v4
import * as React from 'react';
import { memo } from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import Typography from '@material-ui/core/Typography';
import { useRecordContext, sanitizeFieldRestProps, fieldPropTypes } from 'react-admin';
import purify from 'dompurify';

export const removeTags = (input) =>
    input ? input.replace(/<[^>]+>/gm, '') : '';

const RichTextField = memo(
    props => {
        const { className, emptyText, source, stripTags, ...rest } = props;
        const record = useRecordContext(props);
        const value = get(record, source);

        return (
            <Typography
                className={className}
                variant="body2"
                component="span"
                {...sanitizeFieldRestProps(rest)}
            >
                {value == null && emptyText ? (
                    emptyText
                ) : stripTags ? (
                    removeTags(value)
                ) : (
                    <span
                        dangerouslySetInnerHTML={{
                            __html: purify.sanitize(value),
                        }}
                    />
                )}
            </Typography>
        );
    }
);

RichTextField.defaultProps = {
    addLabel: true,
    stripTags: false,
};

RichTextField.propTypes = {
    // @ts-ignore
    ...Typography.propTypes,
    ...fieldPropTypes,
    stripTags: PropTypes.bool,
};

RichTextField.displayName = 'RichTextField';

export default RichTextField;
```

### References

https://github.com/marmelab/react-admin/pull/8644, https://github.com/marmelab/react-admin/pull/8645