React-Admin vulnerable to Cross-Site-Scripting attack on `<RichTextField>`
Description
react-admin's renders HTML via dangerouslySetInnerHTML without sanitization, enabling stored XSS if server-side sanitization is absent.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
react-admin's renders HTML via dangerouslySetInnerHTML without sanitization, enabling stored XSS if server-side sanitization is absent.
Vulnerability
Overview
CVE-2023-25572 is a cross-site scripting (XSS) vulnerability in react-admin and its UI library ra-ui-materialui, affecting versions prior to 3.19.12 and 4.7.6. The root cause is the ` component, which outputs user-supplied HTML using React's dangerouslySetInnerHTML` without any client-side sanitization [2]. If the data is not sanitized server-side, an attacker can inject arbitrary HTML and JavaScript that will be executed in the context of the victim's browser.
Exploitation
An attacker can exploit this vulnerability by providing malicious HTML content (e.g., via a rich text field in a form) that is stored and later rendered by `. No authentication is required if the application allows unauthenticated users to submit data that is displayed. The attack surface includes any React application built with react-admin that uses and does not sanitize HTML on the server [4]. A proof-of-concept demonstrates injection via onclick, onmouseover, and onerror` event handlers [4].
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the browser of any user viewing the affected content. This can lead to credential theft, session hijacking, defacement, or other malicious actions, depending on the application's context.
Mitigation
The vulnerability is fixed in react-admin versions 3.19.12 and 4.7.6, which now use the DOMPurify library to sanitize HTML before rendering [3]. Users who already sanitize HTML server-side do not need to upgrade. As a workaround, developers can replace `` with a custom component that performs sanitization manually [4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
react-adminnpm | < 3.19.12 | 3.19.12 |
react-adminnpm | >= 4.0.0, < 4.7.6 | 4.7.6 |
ra-ui-materialuinpm | >= 4.0.0, < 4.7.6 | 4.7.6 |
ra-ui-materialuinpm | < 3.19.12 | 3.19.12 |
Affected products
3- ghsa-coords2 versions
>= 4.0.0, < 4.7.6+ 1 more
- (no CPE)range: >= 4.0.0, < 4.7.6
- (no CPE)range: < 3.19.12
- Range: < 3.19.12
Patches
416 files changed · +43 −43
examples/data-generator/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "data-generator-retail", - "version": "3.19.11", + "version": "3.19.12", "homepage": "https://github.com/marmelab/react-admin/tree/master/examples/data-generator", "bugs": "https://github.com/marmelab/react-admin/issues", "license": "MIT", @@ -17,7 +17,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^3.19.11", + "ra-core": "^3.19.12", "rimraf": "^2.6.3" }, "peerDependencies": {
examples/simple/package.json+7 −7 modified@@ -1,6 +1,6 @@ { "name": "simple", - "version": "3.19.11", + "version": "3.19.12", "private": true, "scripts": { "dev": "./node_modules/.bin/webpack-dev-server --progress --color --hot --watch --mode development", @@ -11,13 +11,13 @@ "dependencies": { "@material-ui/core": "^4.12.1", "@material-ui/icons": "^4.11.2", - "ra-data-fakerest": "^3.19.11", - "ra-i18n-polyglot": "^3.19.11", - "ra-input-rich-text": "^3.19.11", - "ra-language-english": "^3.19.11", - "ra-language-french": "^3.19.11", + "ra-data-fakerest": "^3.19.12", + "ra-i18n-polyglot": "^3.19.12", + "ra-input-rich-text": "^3.19.12", + "ra-language-english": "^3.19.12", + "ra-language-french": "^3.19.12", "react": "^17.0.0", - "react-admin": "^3.19.11", + "react-admin": "^3.19.12", "react-dom": "^17.0.0" }, "devDependencies": {
lerna.json+1 −1 modified@@ -5,5 +5,5 @@ "examples/simple", "packages/*" ], - "version": "3.19.11" + "version": "3.19.12" }
packages/ra-core/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-core", - "version": "3.19.11", + "version": "3.19.12", "description": "Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md", @@ -40,7 +40,7 @@ "final-form": "^4.20.4", "history": "^4.7.2", "ignore-styles": "~5.0.1", - "ra-test": "^3.19.11", + "ra-test": "^3.19.12", "react": "^17.0.0", "react-dom": "^17.0.0", "react-final-form": "^6.5.7",
packages/ra-data-fakerest/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-fakerest", - "version": "3.19.11", + "version": "3.19.12", "description": "JSON Server data provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -41,7 +41,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^3.19.11", + "ra-core": "^3.19.12", "rimraf": "^2.6.3" }, "peerDependencies": {
packages/ra-data-json-server/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-json-server", - "version": "3.19.11", + "version": "3.19.12", "description": "JSON Server data provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "^5.1.1", - "ra-core": "^3.19.11" + "ra-core": "^3.19.12" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-data-localstorage/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-local-storage", - "version": "3.19.11", + "version": "3.19.12", "description": "Local storage data provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -38,7 +38,7 @@ }, "dependencies": { "lodash": "~4.17.5", - "ra-data-fakerest": "^3.19.11" + "ra-data-fakerest": "^3.19.12" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-data-simple-rest/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-simple-rest", - "version": "3.19.11", + "version": "3.19.12", "description": "Simple REST data provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -29,7 +29,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^3.19.11", + "ra-core": "^3.19.12", "rimraf": "^2.6.3" }, "peerDependencies": {
packages/ra-i18n-polyglot/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-i18n-polyglot", - "version": "3.19.11", + "version": "3.19.12", "description": "Polyglot i18n provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "node-polyglot": "^2.2.2", - "ra-core": "^3.19.11" + "ra-core": "^3.19.12" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-input-rich-text/package.json+3 −3 modified@@ -1,6 +1,6 @@ { "name": "ra-input-rich-text", - "version": "3.19.11", + "version": "3.19.12", "description": "<RichTextInput> component for react-admin, useful for editing HTML code in admin GUIs.", "main": "lib/index.js", "module": "esm/index.js", @@ -53,8 +53,8 @@ }, "devDependencies": { "@types/prop-types": "^15.6.0", - "ra-core": "^3.19.11", - "ra-ui-materialui": "^3.19.11", + "ra-core": "^3.19.12", + "ra-ui-materialui": "^3.19.12", "rimraf": "^2.6.3" } }
packages/ra-language-english/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-language-english", - "version": "3.19.11", + "version": "3.19.12", "description": "English messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -22,7 +22,7 @@ "watch": "tsc --outDir esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^3.19.11" + "ra-core": "^3.19.12" }, "keywords": [ "react",
packages/ra-language-french/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-language-french", - "version": "3.19.11", + "version": "3.19.12", "description": "French messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -16,7 +16,7 @@ "watch": "tsc --outDir esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^3.19.11" + "ra-core": "^3.19.12" }, "keywords": [ "react",
packages/ra-no-code/package.json+4 −4 modified@@ -1,6 +1,6 @@ { "name": "ra-no-code", - "version": "3.19.11", + "version": "3.19.12", "description": "", "files": [ "*.md", @@ -33,7 +33,7 @@ "@types/react-router-dom": "^5.1.0", "cross-env": "^5.2.0", "history": "^4.7.2", - "ra-test": "^3.19.11", + "ra-test": "^3.19.12", "react": "^17.0.0", "react-dom": "^17.0.0", "react-router": "^5.1.0", @@ -51,7 +51,7 @@ "lodash": "~4.17.5", "papaparse": "^5.3.0", "prop-types": "^15.6.1", - "ra-data-local-storage": "^3.19.11", - "react-admin": "^3.19.11" + "ra-data-local-storage": "^3.19.12", + "react-admin": "^3.19.12" } }
packages/ra-test/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-test", - "version": "3.19.11", + "version": "3.19.12", "description": "Test utilities for react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md", @@ -39,7 +39,7 @@ "final-form": "^4.20.4", "history": "^4.7.2", "ignore-styles": "~5.0.1", - "ra-core": "^3.19.11", + "ra-core": "^3.19.12", "react": "^17.0.0", "react-dom": "^17.0.0", "react-redux": "^7.1.0",
packages/ra-ui-materialui/package.json+3 −3 modified@@ -1,6 +1,6 @@ { "name": "ra-ui-materialui", - "version": "3.19.11", + "version": "3.19.12", "description": "UI Components for react-admin with MaterialUI", "files": [ "*.md", @@ -36,8 +36,8 @@ "final-form": "^4.20.4", "final-form-arrays": "^3.0.2", "ignore-styles": "~5.0.1", - "ra-core": "^3.19.11", - "ra-test": "^3.19.11", + "ra-core": "^3.19.12", + "ra-test": "^3.19.12", "react": "^17.0.0", "react-dom": "^17.0.0", "react-final-form": "^6.5.7",
packages/react-admin/package.json+5 −5 modified@@ -1,6 +1,6 @@ { "name": "react-admin", - "version": "3.19.11", + "version": "3.19.12", "description": "A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI", "files": [ "*.md", @@ -40,10 +40,10 @@ "connected-react-router": "^6.5.2", "final-form": "^4.20.4", "final-form-arrays": "^3.0.2", - "ra-core": "^3.19.11", - "ra-i18n-polyglot": "^3.19.11", - "ra-language-english": "^3.19.11", - "ra-ui-materialui": "^3.19.11", + "ra-core": "^3.19.12", + "ra-i18n-polyglot": "^3.19.12", + "ra-language-english": "^3.19.12", + "ra-ui-materialui": "^3.19.12", "react-final-form": "^6.5.7", "react-final-form-arrays": "^3.1.3", "react-redux": "^7.1.0",
16 files changed · +44 −44
examples/data-generator/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "data-generator-retail", - "version": "4.7.2", + "version": "4.7.6", "homepage": "https://github.com/marmelab/react-admin/tree/master/examples/data-generator", "bugs": "https://github.com/marmelab/react-admin/issues", "license": "MIT", @@ -19,7 +19,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.7.2", + "ra-core": "^4.7.6", "rimraf": "^3.0.2", "typescript": "^4.4.0" },
examples/simple/package.json+7 −7 modified@@ -1,6 +1,6 @@ { "name": "simple", - "version": "4.7.5", + "version": "4.7.6", "private": true, "scripts": { "dev": "webpack-dev-server --progress --color --hot --mode development", @@ -18,13 +18,13 @@ "lodash": "~4.17.5", "prop-types": "^15.7.2", "proxy-polyfill": "^0.3.0", - "ra-data-fakerest": "^4.7.2", - "ra-i18n-polyglot": "^4.7.2", - "ra-input-rich-text": "^4.7.5", - "ra-language-english": "^4.7.2", - "ra-language-french": "^4.7.2", + "ra-data-fakerest": "^4.7.6", + "ra-i18n-polyglot": "^4.7.6", + "ra-input-rich-text": "^4.7.6", + "ra-language-english": "^4.7.6", + "ra-language-french": "^4.7.6", "react": "^17.0.0", - "react-admin": "^4.7.5", + "react-admin": "^4.7.6", "react-dom": "^17.0.0", "react-hook-form": "^7.40.0", "react-query": "^3.32.1",
lerna.json+1 −1 modified@@ -5,5 +5,5 @@ "examples/simple", "packages/*" ], - "version": "4.7.5" + "version": "4.7.6" }
packages/ra-core/package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "ra-core", - "version": "4.7.2", + "version": "4.7.6", "description": "Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md",
packages/ra-data-fakerest/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-fakerest", - "version": "4.7.2", + "version": "4.7.6", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -41,7 +41,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.7.2", + "ra-core": "^4.7.6", "rimraf": "^3.0.2", "typescript": "^4.4.0" },
packages/ra-data-json-server/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-json-server", - "version": "4.7.2", + "version": "4.7.6", "description": "JSON Server data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "^7.1.1", - "ra-core": "^4.7.2" + "ra-core": "^4.7.6" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-data-localforage/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-local-forage", - "version": "4.7.2", + "version": "4.7.6", "description": "LocalForage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -42,7 +42,7 @@ "dependencies": { "localforage": "^1.7.1", "lodash": "~4.17.5", - "ra-data-fakerest": "^4.7.2" + "ra-data-fakerest": "^4.7.6" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-data-localstorage/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-local-storage", - "version": "4.7.2", + "version": "4.7.6", "description": "Local storage data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -38,7 +38,7 @@ }, "dependencies": { "lodash": "~4.17.5", - "ra-data-fakerest": "^4.7.2" + "ra-data-fakerest": "^4.7.6" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-data-simple-rest/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-data-simple-rest", - "version": "4.7.2", + "version": "4.7.6", "description": "Simple REST data provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -29,7 +29,7 @@ }, "devDependencies": { "cross-env": "^5.2.0", - "ra-core": "^4.7.2", + "ra-core": "^4.7.6", "rimraf": "^3.0.2", "typescript": "^4.4.0" },
packages/ra-i18n-polyglot/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-i18n-polyglot", - "version": "4.7.2", + "version": "4.7.6", "description": "Polyglot i18n provider for react-admin", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "node-polyglot": "^2.2.2", - "ra-core": "^4.7.2" + "ra-core": "^4.7.6" }, "devDependencies": { "cross-env": "^5.2.0",
packages/ra-input-rich-text/package.json+5 −5 modified@@ -1,6 +1,6 @@ { "name": "ra-input-rich-text", - "version": "4.7.5", + "version": "4.7.6", "description": "<RichTextInput> component for react-admin, useful for editing HTML code in admin GUIs.", "author": "Gildas Garcia", "repository": "marmelab/react-admin", @@ -49,10 +49,10 @@ "@mui/icons-material": "^5.0.1", "@mui/material": "^5.0.2", "@testing-library/react": "^11.2.3", - "data-generator-retail": "^4.7.2", - "ra-core": "^4.7.2", - "ra-data-fakerest": "^4.7.2", - "ra-ui-materialui": "^4.7.5", + "data-generator-retail": "^4.7.6", + "ra-core": "^4.7.6", + "ra-data-fakerest": "^4.7.6", + "ra-ui-materialui": "^4.7.6", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.40.0",
packages/ra-language-english/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-language-english", - "version": "4.7.2", + "version": "4.7.6", "description": "English messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -21,7 +21,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.7.2" + "ra-core": "^4.7.6" }, "devDependencies": { "rimraf": "^3.0.2",
packages/ra-language-french/package.json+2 −2 modified@@ -1,6 +1,6 @@ { "name": "ra-language-french", - "version": "4.7.2", + "version": "4.7.6", "description": "French messages for react-admin, the frontend framework for building admin applications on top of REST/GraphQL services", "repository": { "type": "git", @@ -16,7 +16,7 @@ "watch": "tsc --outDir dist/esm --module es2015 --watch" }, "dependencies": { - "ra-core": "^4.7.2" + "ra-core": "^4.7.6" }, "devDependencies": { "rimraf": "^3.0.2",
packages/ra-no-code/package.json+3 −3 modified@@ -1,6 +1,6 @@ { "name": "ra-no-code", - "version": "4.7.5", + "version": "4.7.6", "description": "", "files": [ "*.md", @@ -48,8 +48,8 @@ "lodash": "~4.17.5", "papaparse": "^5.3.0", "prop-types": "^15.6.1", - "ra-data-local-storage": "^4.7.2", - "react-admin": "^4.7.5", + "ra-data-local-storage": "^4.7.6", + "react-admin": "^4.7.6", "react-dropzone": "^12.0.4", "react-query": "^3.32.1" }
packages/ra-ui-materialui/package.json+4 −4 modified@@ -1,6 +1,6 @@ { "name": "ra-ui-materialui", - "version": "4.7.5", + "version": "4.7.6", "description": "UI Components for react-admin with MUI", "files": [ "*.md", @@ -34,9 +34,9 @@ "file-api": "~0.10.4", "history": "^5.1.0", "ignore-styles": "~5.0.1", - "ra-core": "^4.7.2", - "ra-i18n-polyglot": "^4.7.2", - "ra-language-english": "^4.7.2", + "ra-core": "^4.7.6", + "ra-i18n-polyglot": "^4.7.6", + "ra-language-english": "^4.7.6", "react": "^17.0.0", "react-dom": "^17.0.0", "react-hook-form": "^7.40.0",
packages/react-admin/package.json+5 −5 modified@@ -1,6 +1,6 @@ { "name": "react-admin", - "version": "4.7.5", + "version": "4.7.6", "description": "A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI", "files": [ "*.md", @@ -41,10 +41,10 @@ "@mui/icons-material": "^5.0.1", "@mui/material": "^5.0.2", "history": "^5.1.0", - "ra-core": "^4.7.2", - "ra-i18n-polyglot": "^4.7.2", - "ra-language-english": "^4.7.2", - "ra-ui-materialui": "^4.7.5", + "ra-core": "^4.7.6", + "ra-i18n-polyglot": "^4.7.6", + "ra-language-english": "^4.7.6", + "ra-ui-materialui": "^4.7.6", "react-hook-form": "^7.40.0", "react-router": "^6.1.0", "react-router-dom": "^6.1.0"
c1891afc5453Merge pull request #8644 from marmelab/fix-RichTextField-XSS
6 files changed · +136 −5
docs/RichTextField.md+8 −3 modified@@ -7,23 +7,28 @@ title: "The RichTextField Component" This component displays some HTML content. The content is "rich" (i.e. unescaped) by default. + + +This component leverages [the `dangerouslySetInnerHTML` attribute](https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml), but uses [the DomPurify library](https://github.com/cure53/DOMPurify) to sanitize the HTML before rendering it. It means it is **safe from Cross-Site Scripting (XSS) attacks** - but it's still a good practice to sanitize the value server-side. + +## Usage + ```jsx import { RichTextField } from 'react-admin'; <RichTextField source="body" /> ``` - -## Properties +## Props | Prop | Required | Type | Default | Description | | ----------- | -------- | --------- | -------- | ---------------------------------------------------- | | `stripTags` | Optional | `boolean` | `false` | If `true`, remove all HTML tags and render text only | `<RichTextField>` also accepts the [common field props](./Fields.md#common-field-props). -## Usage +## `stripTags` The `stripTags` prop allows to remove all HTML markup, preventing some display glitches (which is especially useful in list views, or when truncating the content).
packages/ra-ui-materialui/package.json+1 −0 modified@@ -60,6 +60,7 @@ "autosuggest-highlight": "^3.1.1", "clsx": "^1.1.1", "css-mediaquery": "^0.1.2", + "dompurify": "^2.4.3", "inflection": "~1.12.0", "jsonexport": "^3.2.0", "lodash": "~4.17.5",
packages/ra-ui-materialui/src/field/RichTextField.spec.tsx+14 −1 modified@@ -1,9 +1,10 @@ import * as React from 'react'; import expect from 'expect'; -import { render } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import { RecordContextProvider } from 'ra-core'; import { RichTextField, removeTags } from './RichTextField'; +import { Secure } from './RichTextField.stories'; describe('stripTags', () => { it('should strip HTML tags from input', () => { @@ -135,4 +136,16 @@ describe('<RichTextField />', () => { expect(queryByText('NA')).not.toBeNull(); } ); + + it('should be safe by default', async () => { + const { container } = render(<Secure />); + fireEvent.mouseOver( + screen.getByText( + "It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature." + ) + ); + expect( + (container.querySelector('#stolendata') as HTMLInputElement)?.value + ).toEqual('none'); + }); });
packages/ra-ui-materialui/src/field/RichTextField.stories.tsx+84 −0 added@@ -0,0 +1,84 @@ +import * as React from 'react'; +import { RecordContextProvider, useTimeout } from 'ra-core'; +import dompurify from 'dompurify'; + +import { RichTextField } from './RichTextField'; +import { SimpleShowLayout } from '../detail/SimpleShowLayout'; + +export default { + title: 'ra-ui-materialui/fields/RichTextField', +}; + +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">Leo Tolstoy</a>, +published serially, then in its entirety in 1869. +</p> +<p> +It is regarded as one of Tolstoy's finest literary achievements and remains a classic of world literature. +</p> +<img src="https://upload.wikimedia.org/wikipedia/commons/a/af/Tolstoy_-_War_and_Peace_-_first_edition%2C_1869.jpg" /> +`, +}; + +export const Basic = () => ( + <RecordContextProvider value={record}> + <RichTextField source="body" /> + </RecordContextProvider> +); + +export const StripTags = () => ( + <RecordContextProvider value={record}> + <RichTextField source="body" stripTags /> + </RecordContextProvider> +); + +export const InSimpleShowLayout = () => ( + <RecordContextProvider value={record}> + <SimpleShowLayout> + <RichTextField source="body" /> + </SimpleShowLayout> + </RecordContextProvider> +); + +const DomPurifyInspector = () => { + useTimeout(100); // force a redraw after the lazy loading of dompurify + const dompurifyRemoved = dompurify.removed + .map( + removal => + `removed attribute ${ + removal.attribute.name + } from tag <${removal.from.tagName.toLowerCase()}>` + ) + .join(', '); + return <em>{dompurifyRemoved}</em>; +}; + +export const Secure = () => ( + <RecordContextProvider + value={{ + 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';" /> +`, + }} + > + <RichTextField source="body" /> + <hr /> + <DomPurifyInspector /> + <div> + <h4>Stolen data:</h4> + <input id="stolendata" defaultValue="none" /> + </div> + </RecordContextProvider> +);
packages/ra-ui-materialui/src/field/RichTextField.tsx+21 −1 modified@@ -4,10 +4,26 @@ import PropTypes from 'prop-types'; import get from 'lodash/get'; import Typography, { TypographyProps } from '@mui/material/Typography'; import { useRecordContext } from 'ra-core'; +import purify from 'dompurify'; import { sanitizeFieldRestProps } from './sanitizeFieldRestProps'; import { InjectedFieldProps, PublicFieldProps, fieldPropTypes } from './types'; +/** + * Render an HTML string as rich text + * + * Note: This component leverages the `dangerouslySetInnerHTML` attribute, + * but uses the DomPurify library to sanitize the HTML before rendering it. + * + * It means it is safe from Cross-Site Scripting (XSS) attacks - but it's still + * a good practice to sanitize the value server-side. + * + * @example + * <RichTextField source="description" /> + * + * @example // remove all tags and output text only + * <RichTextField source="description" stripTags /> + */ export const RichTextField: FC<RichTextFieldProps> = memo<RichTextFieldProps>( props => { const { @@ -32,7 +48,11 @@ export const RichTextField: FC<RichTextFieldProps> = memo<RichTextFieldProps>( ) : stripTags ? ( removeTags(value) ) : ( - <span dangerouslySetInnerHTML={{ __html: value }} /> + <span + dangerouslySetInnerHTML={{ + __html: purify.sanitize(value), + }} + /> )} </Typography> );
yarn.lock+8 −0 modified@@ -11655,6 +11655,13 @@ __metadata: languageName: node linkType: hard +"dompurify@npm:^2.4.3": + version: 2.4.3 + resolution: "dompurify@npm:2.4.3" + checksum: 4c93f5bc8855bbe7dcb33487c0b252a00309fbd8a6d0ec280abbc3af695b43d1bf7f526c2f323fa697314b0b3de3511c756005dddc6ed90d1a1440a3d6ff89d9 + languageName: node + linkType: hard + "domutils@npm:^1.7.0": version: 1.7.0 resolution: "domutils@npm:1.7.0" @@ -21922,6 +21929,7 @@ __metadata: clsx: ^1.1.1 cross-env: ^5.2.0 css-mediaquery: ^0.1.2 + dompurify: ^2.4.3 expect: ^27.4.6 file-api: ~0.10.4 history: ^5.1.0
a4619a8691c2Merge pull request #8645 from marmelab/fix-RichTextField-XSS-v3
5 files changed · +58 −50
.github/workflows/codeql-analysis.yml+45 −48 modified@@ -1,52 +1,49 @@ -name: "Code scanning - action" +name: 'Code scanning - action' on: - push: - pull_request: - schedule: - - cron: '0 10 * * 3' + push: + pull_request: + schedule: + - cron: '0 10 * * 3' jobs: - CodeQL-Build: - - # CodeQL runs on ubuntu-latest and windows-latest - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + CodeQL-Build: + # CodeQL runs on ubuntu-latest and windows-latest + runs-on: ubuntu-latest + permissions: + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + + + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2
packages/ra-core/src/controller/index.ts+1 −1 modified@@ -29,6 +29,7 @@ import useListSortContext from './useListSortContext'; export type { ListControllerProps, + ListPaginationContextValue, PaginationHookResult, SortProps, UseReferenceProps, @@ -42,7 +43,6 @@ export { ListContext, ListFilterContext, ListPaginationContext, - ListPaginationContextValue, ListSortContext, ListContextProvider, useCheckMinimumRequiredProps,
packages/ra-ui-materialui/package.json+1 −0 modified@@ -70,6 +70,7 @@ "classnames": "~2.2.5", "connected-react-router": "^6.5.2", "css-mediaquery": "^0.1.2", + "dompurify": "^2.4.3", "downshift": "3.2.7", "inflection": "~1.13.1", "jsonexport": "^2.4.1",
packages/ra-ui-materialui/src/field/RichTextField.tsx+6 −1 modified@@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import get from 'lodash/get'; import Typography, { TypographyProps } from '@material-ui/core/Typography'; import { useRecordContext } from 'ra-core'; +import purify from 'dompurify'; import sanitizeFieldRestProps from './sanitizeFieldRestProps'; import { InjectedFieldProps, PublicFieldProps, fieldPropTypes } from './types'; @@ -29,7 +30,11 @@ const RichTextField: FC<RichTextFieldProps> = memo<RichTextFieldProps>( ) : stripTags ? ( removeTags(value) ) : ( - <span dangerouslySetInnerHTML={{ __html: value }} /> + <span + dangerouslySetInnerHTML={{ + __html: purify.sanitize(value), + }} + /> )} </Typography> );
yarn.lock+5 −0 modified@@ -8144,6 +8144,11 @@ domhandler@^4.2.0, domhandler@^4.3.0: dependencies: domelementtype "^2.2.0" +dompurify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.3.tgz#f4133af0e6a50297fc8874e2eaedc13a3c308c03" + integrity sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ== + domutils@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
Vulnerability mechanics
Root cause
"The `"
Attack vector
An attacker who can control or inject data into a field rendered by `
Affected code
The vulnerability resides in the `RichTextField` component at `packages/ra-ui-materialui/src/field/RichTextField.tsx`. The component uses React's `dangerouslySetInnerHTML` to render HTML content from the record field, but prior to the patch it did not sanitize the HTML before outputting it. The fix adds a call to `purify.sanitize(value)` (using the DOMPurify library) to strip malicious content before rendering [patch_id=1641047][patch_id=1641053].
What the fix does
The patch imports the `dompurify` library and wraps the HTML value passed to `dangerouslySetInnerHTML` with `purify.sanitize(value)` [patch_id=1641047][patch_id=1641053]. DOMPurify removes event handlers (e.g., `onclick`, `onmouseover`, `onerror`), malicious URLs, and other XSS vectors from the HTML string before React renders it. The `yarn.lock` and `package.json` files are updated to add `dompurify` as a dependency [patch_id=1641047][patch_id=1641053]. A new story (`Secure`) and a test confirm that injected event handlers are stripped and cannot exfiltrate data [patch_id=1641047].
Preconditions
- configThe application uses the `` component to display user-controllable or untrusted HTML data.
- inputThe HTML data is not sanitized server-side before being stored or returned by the API.
- inputThe attacker can inject arbitrary HTML/JavaScript into the field value (e.g., via a form submission, API injection, or stored XSS vector).
Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-5jcr-82fh-339vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-25572ghsaADVISORY
- github.com/marmelab/react-admin/pull/8644ghsax_refsource_MISCWEB
- github.com/marmelab/react-admin/pull/8645ghsax_refsource_MISCWEB
- github.com/marmelab/react-admin/releases/tag/v3.19.12ghsax_refsource_MISCWEB
- github.com/marmelab/react-admin/releases/tag/v4.7.6ghsax_refsource_MISCWEB
- github.com/marmelab/react-admin/security/advisories/GHSA-5jcr-82fh-339vghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.