diff options
| author | Adam <[email protected]> | 2026-01-12 15:50:11 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-01-12 15:50:24 -0600 |
| commit | d7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4 (patch) | |
| tree | 0d4797d4648d74b84daff851422d65eaf9818458 /packages | |
| parent | 5c4345da4f896c40089a7254eaaedf718739202c (diff) | |
| download | opencode-d7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4.tar.gz opencode-d7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4.zip | |
fix(app): sanitize markdown -> html
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/ui/package.json | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/markdown.tsx | 34 |
2 files changed, 33 insertions, 2 deletions
diff --git a/packages/ui/package.json b/packages/ui/package.json index bc37a826e..5b440f515 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -51,6 +51,7 @@ "fuzzysort": "catalog:", "katex": "0.16.27", "luxon": "catalog:", + "dompurify": "catalog:", "marked": "catalog:", "marked-katex-extension": "5.1.6", "marked-shiki": "catalog:", diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 2b0b01874..3aefe04da 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -1,6 +1,8 @@ import { useMarked } from "../context/marked" +import DOMPurify from "dompurify" import { checksum } from "@opencode-ai/util/encode" import { ComponentProps, createResource, splitProps } from "solid-js" +import { isServer } from "solid-js/web" type Entry = { hash: string @@ -10,6 +12,31 @@ type Entry = { const max = 200 const cache = new Map<string, Entry>() +if (typeof window !== "undefined" && DOMPurify.isSupported) { + DOMPurify.addHook("afterSanitizeAttributes", (node: Element) => { + if (!(node instanceof HTMLAnchorElement)) return + if (node.target !== "_blank") return + + const rel = node.getAttribute("rel") ?? "" + const set = new Set(rel.split(/\s+/).filter(Boolean)) + set.add("noopener") + set.add("noreferrer") + node.setAttribute("rel", Array.from(set).join(" ")) + }) +} + +const config = { + USE_PROFILES: { html: true, mathMl: true }, + SANITIZE_NAMED_PROPS: true, + FORBID_TAGS: ["style"], + FORBID_CONTENTS: ["style", "script"], +} + +function sanitize(html: string) { + if (!DOMPurify.isSupported) return "" + return DOMPurify.sanitize(html, config) +} + function touch(key: string, value: Entry) { cache.delete(key) cache.set(key, value) @@ -34,6 +61,8 @@ export function Markdown( const [html] = createResource( () => local.text, async (markdown) => { + if (isServer) return "" + const hash = checksum(markdown) const key = local.cacheKey ?? hash @@ -46,8 +75,9 @@ export function Markdown( } const next = await marked.parse(markdown) - if (key && hash) touch(key, { hash, html: next }) - return next + const safe = sanitize(next) + if (key && hash) touch(key, { hash, html: safe }) + return safe }, { initialValue: "" }, ) |
