Skip to content

Commit e1fc1aa

Browse files
author
Eric Wheeler
committed
fix: eliminate XSS vulnerability in CodeBlock component
Replace dangerouslySetInnerHTML with safer codeToHast approach to render syntax-highlighted code from Shiki. This eliminates the cross-site scripting vulnerability while maintaining identical rendering output and performance. Security considerations: - Eliminates potential for HTML injection attacks - Maintains all syntax highlighting capabilities - Preserves exact visual output Performance considerations: - Direct React element creation is more efficient than HTML parsing - No browser HTML parsing overhead - Memoization pattern preserved for optimal rendering This issue was discovered as part of security review #3785. Fixes: #5156 Signed-off-by: Eric Wheeler <[email protected]>
1 parent 992997c commit e1fc1aa

File tree

3 files changed

+20
-6
lines changed

3 files changed

+20
-6
lines changed

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"debounce": "^2.1.1",
4343
"fast-deep-equal": "^3.1.3",
4444
"fzf": "^0.5.2",
45+
"hast-util-to-jsx-runtime": "^2.3.6",
4546
"i18next": "^25.0.0",
4647
"i18next-http-backend": "^3.0.2",
4748
"katex": "^0.16.11",

webview-ui/src/components/common/CodeBlock.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useCopyToClipboard } from "@src/utils/clipboard"
44
import { getHighlighter, isLanguageLoaded, normalizeLanguage, ExtendedLanguage } from "@src/utils/highlighter"
55
import { bundledLanguages } from "shiki"
66
import type { ShikiTransformer } from "shiki"
7+
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
8+
import { Fragment, jsx, jsxs } from "react/jsx-runtime"
79
import { ChevronDown, ChevronUp, WrapText, AlignJustify, Copy, Check } from "lucide-react"
810
import { useAppTranslation } from "@src/i18n/TranslationContext"
911
import { 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)
@@ -266,7 +268,7 @@ const CodeBlock = memo(
266268
const highlighter = await getHighlighter(currentLanguage)
267269
if (!isMountedRef.current) return
268270

269-
const html = await highlighter.codeToHtml(source || "", {
271+
const hast = await highlighter.codeToHast(source || "", {
270272
lang: currentLanguage || "txt",
271273
theme: document.body.className.toLowerCase().includes("light") ? "github-light" : "github-dark",
272274
transformers: [
@@ -290,8 +292,16 @@ const CodeBlock = memo(
290292
})
291293
if (!isMountedRef.current) return
292294

295+
// Convert HAST to React elements
296+
const reactElement = toJsxRuntime(hast, {
297+
Fragment,
298+
jsx,
299+
jsxs,
300+
// Don't override components - let them render as-is to maintain exact output
301+
})
302+
293303
if (isMountedRef.current) {
294-
setHighlightedCode(html)
304+
setHighlightedCode(reactElement)
295305
}
296306
}
297307

@@ -783,7 +793,7 @@ const CodeBlock = memo(
783793
)
784794

785795
// Memoized content component to prevent unnecessary re-renders of highlighted code
786-
const MemoizedCodeContent = memo(({ html }: { html: string }) => <div dangerouslySetInnerHTML={{ __html: html }} />)
796+
const MemoizedCodeContent = memo(({ children }: { children: React.ReactNode }) => <>{children}</>)
787797

788798
// Memoized StyledPre component
789799
const MemoizedStyledPre = memo(
@@ -801,7 +811,7 @@ const MemoizedStyledPre = memo(
801811
wordWrap: boolean
802812
windowShade: boolean
803813
collapsedHeight?: number
804-
highlightedCode: string
814+
highlightedCode: React.ReactNode
805815
updateCodeBlockButtonPosition: (forceHide?: boolean) => void
806816
}) => (
807817
<StyledPre
@@ -812,7 +822,7 @@ const MemoizedStyledPre = memo(
812822
collapsedHeight={collapsedHeight}
813823
onMouseDown={() => updateCodeBlockButtonPosition(true)}
814824
onMouseUp={() => updateCodeBlockButtonPosition(false)}>
815-
<MemoizedCodeContent html={highlightedCode} />
825+
<MemoizedCodeContent>{highlightedCode}</MemoizedCodeContent>
816826
</StyledPre>
817827
),
818828
)

0 commit comments

Comments
 (0)