@@ -4,6 +4,8 @@ import { useCopyToClipboard } from "@src/utils/clipboard"
44import { getHighlighter , isLanguageLoaded , normalizeLanguage , ExtendedLanguage } from "@src/utils/highlighter"
55import { bundledLanguages } from "shiki"
66import type { ShikiTransformer } from "shiki"
7+ import { toJsxRuntime } from "hast-util-to-jsx-runtime"
8+ import { Fragment , jsx , jsxs } from "react/jsx-runtime"
79import { ChevronDown , ChevronUp , WrapText , AlignJustify , Copy , Check } from "lucide-react"
810import { useAppTranslation } from "@src/i18n/TranslationContext"
911import { StandardTooltip } from "@/components/ui"
@@ -226,7 +228,7 @@ const CodeBlock = memo(
226228 const [ windowShade , setWindowShade ] = useState ( initialWindowShade )
227229 const [ currentLanguage , setCurrentLanguage ] = useState < ExtendedLanguage > ( ( ) => normalizeLanguage ( language ) )
228230 const userChangedLanguageRef = useRef ( false )
229- const [ highlightedCode , setHighlightedCode ] = useState < string > ( "" )
231+ const [ highlightedCode , setHighlightedCode ] = useState < React . ReactNode > ( null )
230232 const [ showCollapseButton , setShowCollapseButton ] = useState ( true )
231233 const codeBlockRef = useRef < HTMLDivElement > ( null )
232234 const preRef = useRef < HTMLDivElement > ( null )
@@ -253,7 +255,12 @@ const CodeBlock = memo(
253255 // Set mounted state at the beginning of this effect
254256 isMountedRef . current = true
255257
256- const fallback = `<pre style="padding: 0; margin: 0;"><code class="hljs language-${ currentLanguage || "txt" } ">${ source || "" } </code></pre>`
258+ // Create a safe fallback using React elements instead of HTML string
259+ const fallback = (
260+ < pre style = { { padding : 0 , margin : 0 } } >
261+ < code className = { `hljs language-${ currentLanguage || "txt" } ` } > { source || "" } </ code >
262+ </ pre >
263+ )
257264
258265 const highlight = async ( ) => {
259266 // Show plain text if language needs to be loaded.
@@ -266,7 +273,7 @@ const CodeBlock = memo(
266273 const highlighter = await getHighlighter ( currentLanguage )
267274 if ( ! isMountedRef . current ) return
268275
269- const html = await highlighter . codeToHtml ( source || "" , {
276+ const hast = await highlighter . codeToHast ( source || "" , {
270277 lang : currentLanguage || "txt" ,
271278 theme : document . body . className . toLowerCase ( ) . includes ( "light" ) ? "github-light" : "github-dark" ,
272279 transformers : [
@@ -290,8 +297,25 @@ const CodeBlock = memo(
290297 } )
291298 if ( ! isMountedRef . current ) return
292299
293- if ( isMountedRef . current ) {
294- setHighlightedCode ( html )
300+ // Convert HAST to React elements using hast-util-to-jsx-runtime
301+ // This approach eliminates XSS vulnerabilities by avoiding dangerouslySetInnerHTML
302+ // while maintaining the exact same visual output and syntax highlighting
303+ try {
304+ const reactElement = toJsxRuntime ( hast , {
305+ Fragment,
306+ jsx,
307+ jsxs,
308+ // Don't override components - let them render as-is to maintain exact output
309+ } )
310+
311+ if ( isMountedRef . current ) {
312+ setHighlightedCode ( reactElement )
313+ }
314+ } catch ( error ) {
315+ console . error ( "[CodeBlock] Error converting HAST to JSX:" , error )
316+ if ( isMountedRef . current ) {
317+ setHighlightedCode ( fallback )
318+ }
295319 }
296320 }
297321
@@ -783,7 +807,7 @@ const CodeBlock = memo(
783807)
784808
785809// Memoized content component to prevent unnecessary re-renders of highlighted code
786- const MemoizedCodeContent = memo ( ( { html } : { html : string } ) => < div dangerouslySetInnerHTML = { { __html : html } } /> )
810+ const MemoizedCodeContent = memo ( ( { children } : { children : React . ReactNode } ) => < > { children } < /> )
787811
788812// Memoized StyledPre component
789813const MemoizedStyledPre = memo (
@@ -801,7 +825,7 @@ const MemoizedStyledPre = memo(
801825 wordWrap : boolean
802826 windowShade : boolean
803827 collapsedHeight ?: number
804- highlightedCode : string
828+ highlightedCode : React . ReactNode
805829 updateCodeBlockButtonPosition : ( forceHide ?: boolean ) => void
806830 } ) => (
807831 < StyledPre
@@ -812,7 +836,7 @@ const MemoizedStyledPre = memo(
812836 collapsedHeight = { collapsedHeight }
813837 onMouseDown = { ( ) => updateCodeBlockButtonPosition ( true ) }
814838 onMouseUp = { ( ) => updateCodeBlockButtonPosition ( false ) } >
815- < MemoizedCodeContent html = { highlightedCode } / >
839+ < MemoizedCodeContent > { highlightedCode } </ MemoizedCodeContent >
816840 </ StyledPre >
817841 ) ,
818842)
0 commit comments