summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-12 15:50:11 -0600
committerAdam <[email protected]>2026-01-12 15:50:24 -0600
commitd7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4 (patch)
tree0d4797d4648d74b84daff851422d65eaf9818458 /packages
parent5c4345da4f896c40089a7254eaaedf718739202c (diff)
downloadopencode-d7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4.tar.gz
opencode-d7a1c268d9b75cb5aea9ad9686d7ac52a3053aa4.zip
fix(app): sanitize markdown -> html
Diffstat (limited to 'packages')
-rw-r--r--packages/ui/package.json1
-rw-r--r--packages/ui/src/components/markdown.tsx34
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: "" },
)