From 23763f0e0f5f655d7876e9a8047ca7818d3dafec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:56:38 +0000 Subject: [PATCH 1/3] Initial plan From 2530076c6abf3512bdea157178452ee11fec822a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:00:35 +0000 Subject: [PATCH 2/3] fix: sanitize HTML in RichText with DOMPurify and harden useLocalStorage - Add DOMPurify to sanitize innerHTML and dangerouslySetInnerHTML in RichText.jsx - Add ALLOWED_TAGS allowlist for dynamic tag name rendering in RichText.jsx - Add type validation for localStorage parsed values in useLocalStorage.js Co-authored-by: flexseth <3792502+flexseth@users.noreply.github.com> --- package-lock.json | 34 ++++++++++++++++++++++++---------- package.json | 1 + src/components/RichText.jsx | 15 ++++++++++----- src/hooks/useLocalStorage.js | 10 +++++++++- 4 files changed, 44 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index b65b840..82530b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "mdx-react-docs", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mdx-react-docs", - "version": "1.0.0", + "version": "2.0.0", "dependencies": { "@mdx-js/react": "^3.1.0", "@mdx-js/rollup": "^3.1.0", + "dompurify": "^3.3.2", "prism-react-renderer": "^2.4.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -53,7 +54,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1345,6 +1345,13 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1383,7 +1390,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1452,7 +1458,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1591,7 +1596,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -1645,6 +1651,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dompurify": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", @@ -3240,7 +3258,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3253,7 +3270,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3477,7 +3493,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -3805,7 +3820,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/package.json b/package.json index d61a594..c7a22f9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@mdx-js/react": "^3.1.0", "@mdx-js/rollup": "^3.1.0", + "dompurify": "^3.3.2", "prism-react-renderer": "^2.4.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/components/RichText.jsx b/src/components/RichText.jsx index b637af1..ae2613c 100644 --- a/src/components/RichText.jsx +++ b/src/components/RichText.jsx @@ -22,8 +22,10 @@ * @param {boolean} [props.disabled=false] - Disables editing. */ import { useRef, useEffect, useState } from 'react'; +import DOMPurify from 'dompurify'; const ALL_FORMATS = [ 'core/bold', 'core/italic', 'core/link' ]; +const ALLOWED_TAGS = [ 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span' ]; function FormatButton( { command, label, children, onFormat } ) { return ( @@ -42,7 +44,7 @@ function FormatButton( { command, label, children, onFormat } ) { } export default function RichText( { - tagName: Tag = 'p', + tagName = 'p', value = '', onChange, placeholder = '', @@ -52,14 +54,16 @@ export default function RichText( { style, disabled = false, } ) { + const Tag = ALLOWED_TAGS.includes( tagName ) ? tagName : 'p'; const editableRef = useRef( null ); const [ isFocused, setIsFocused ] = useState( false ); // Sync external value changes without resetting cursor position. useEffect( () => { const el = editableRef.current; - if ( el && el.innerHTML !== value ) { - el.innerHTML = value || ''; + const sanitized = DOMPurify.sanitize( value || '' ); + if ( el && el.innerHTML !== sanitized ) { + el.innerHTML = sanitized; } }, [ value ] ); @@ -133,16 +137,17 @@ export default function RichText( { * @param {Object} [props.style] - Inline styles. */ RichText.Content = function RichTextContent( { - tagName: Tag = 'p', + tagName = 'p', value = '', className = '', style, } ) { + const Tag = ALLOWED_TAGS.includes( tagName ) ? tagName : 'p'; return ( ); }; diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js index 2710a73..48552d1 100644 --- a/src/hooks/useLocalStorage.js +++ b/src/hooks/useLocalStorage.js @@ -13,7 +13,15 @@ export function useLocalStorage( key, initial ) { const [ storedValue, setStoredValue ] = useState( () => { try { const item = window.localStorage.getItem( key ); - return item !== null ? JSON.parse( item ) : initial; + if ( item === null ) { + return initial; + } + const parsed = JSON.parse( item ); + // Validate that the stored value matches the expected type. + if ( typeof parsed !== typeof initial && initial !== undefined ) { + return initial; + } + return parsed; } catch { return initial; } From 4cee7fe058a61f3b412190463ec5c8c408c39a6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:01:15 +0000 Subject: [PATCH 3/3] fix: improve localStorage type validation for arrays and null Co-authored-by: flexseth <3792502+flexseth@users.noreply.github.com> --- src/hooks/useLocalStorage.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js index 48552d1..b2c0d8e 100644 --- a/src/hooks/useLocalStorage.js +++ b/src/hooks/useLocalStorage.js @@ -18,8 +18,13 @@ export function useLocalStorage( key, initial ) { } const parsed = JSON.parse( item ); // Validate that the stored value matches the expected type. - if ( typeof parsed !== typeof initial && initial !== undefined ) { - return initial; + if ( initial !== undefined && initial !== null ) { + if ( Array.isArray( initial ) !== Array.isArray( parsed ) ) { + return initial; + } + if ( typeof parsed !== typeof initial ) { + return initial; + } } return parsed; } catch {