chore(security): upgrade dompurify and unify sanitizer path

This commit is contained in:
Mega Yu
2026-03-12 15:26:59 +08:00
parent 30a74294c6
commit afe42a4cb5
5 changed files with 93 additions and 15 deletions

View File

@@ -151,7 +151,7 @@
"codemirror": "5.65.18",
"comlink": "^4.4.1",
"d3-force": "3.0.0",
"dompurify": "2.4.0",
"dompurify": "^3.3.3",
"emoji-mart": "^5.5.2",
"fs": "0.0.1-security",
"fs-extra": "^11.3.0",

View File

@@ -3,6 +3,7 @@
[frontend.components.lazy-editor :as lazy-editor]
[frontend.handler.notification :as notification]
[frontend.handler.plugin :as plugin-handler]
[frontend.security :as security]
[frontend.ui :as ui]
[frontend.util :as util]
[goog.functions :refer [debounce]]
@@ -10,17 +11,10 @@
[logseq.shui.ui :as shui]
[rum.core :as rum]))
(defn- dom-purify
[html opts]
(try
(js-invoke js/DOMPurify "sanitize" html (bean/->js opts))
(catch js/Error e
(js/console.warn e) html)))
(rum/defc html-content
[html]
[:div.html-content.pl-1.flex-1.text-sm
{:dangerouslySetInnerHTML {:__html (dom-purify html nil)}}])
{:dangerouslySetInnerHTML {:__html (security/sanitize-html html)}}])
(rum/defc edit-settings-file
[pid {:keys [class edit-mode set-edit-mode!]}]

View File

@@ -1,6 +1,36 @@
(ns frontend.security
"Provide security focused fns like preventing XSS attacks"
(:require ["dompurify" :as DOMPurify]))
(:require ["dompurify" :as dompurify]))
(defn- sanitizer-instance?
[value]
(fn? (some-> value (aget "sanitize"))))
(defn- resolve-dompurify
[module]
(let [purify (or (.-default module) module)]
(cond
(sanitizer-instance? purify)
purify
(fn? purify)
(let [instance (purify js/window)]
(if (sanitizer-instance? instance)
instance
(throw (js/Error. "DOMPurify factory did not return a sanitizer instance"))))
:else
(throw (js/Error. "Unsupported DOMPurify module shape")))))
(defonce ^:private dompurify-instance (volatile! nil))
(defn- get-dompurify
([] (get-dompurify dompurify dompurify-instance))
([module cache]
(or @cache
(let [instance (resolve-dompurify module)]
(vreset! cache instance)
instance))))
(def sanitization-options (clj->js {:ADD_TAGS ["iframe"]
:ADD_ATTR ["is"]
@@ -8,4 +38,4 @@
(defn sanitize-html
[html]
(.sanitize DOMPurify html sanitization-options))
(js-invoke (get-dompurify) "sanitize" html sanitization-options))

View File

@@ -0,0 +1,47 @@
(ns frontend.security-test
(:require [cljs.test :refer [deftest is testing]]
[frontend.security :as security]))
(deftest sanitize-html-uses-logseq-sanitization-policy
(testing "sanitize-html delegates to DOMPurify with the repository's supported plugin policy"
(let [called (atom nil)
html "<p onclick=\"alert('x')\">safe</p><iframe src=\"logseq://plugin/frame\" is=\"plugin-frame\"></iframe><script>alert('x')</script>"
fake-purify #js {:sanitize (fn [input opts]
(reset! called {:input input
:opts (js->clj opts)})
"<p>safe</p><iframe src=\"logseq://plugin/frame\" is=\"plugin-frame\"></iframe>")}]
(with-redefs [security/get-dompurify (fn [] fake-purify)]
(is (= "<p>safe</p><iframe src=\"logseq://plugin/frame\" is=\"plugin-frame\"></iframe>"
(security/sanitize-html html)))
(is (= html (:input @called)))
(is (= ["iframe"] (get-in @called [:opts "ADD_TAGS"])))
(is (= ["is"] (get-in @called [:opts "ADD_ATTR"])))
(is (= true (get-in @called [:opts "ALLOW_UNKNOWN_PROTOCOLS"])))))))
(deftest resolve-dompurify-fails-fast-on-unsupported-shapes
(testing "unsupported module shapes fail explicitly instead of falling through to a later sanitize call"
(let [bad-module-error (try
(#'security/resolve-dompurify #js {})
nil
(catch js/Error error
error))
bad-factory-error (try
(#'security/resolve-dompurify (fn [_] #js {}))
nil
(catch js/Error error
error))]
(is (= "Unsupported DOMPurify module shape" (.-message bad-module-error)))
(is (= "DOMPurify factory did not return a sanitizer instance"
(.-message bad-factory-error))))))
(deftest get-dompurify-caches-the-resolved-instance
(testing "the DOMPurify instance is resolved once and then reused"
(let [calls (atom 0)
cache (volatile! nil)
fake-instance #js {:sanitize (fn [_ _] "<p>safe</p>")}]
(with-redefs [security/resolve-dompurify (fn [_]
(swap! calls inc)
fake-instance)]
(is (identical? fake-instance (#'security/get-dompurify #js {} cache)))
(is (identical? fake-instance (#'security/get-dompurify #js {} cache)))
(is (= 1 @calls))))))

View File

@@ -1493,6 +1493,11 @@
dependencies:
"@types/node" "*"
"@types/trusted-types@^2.0.7":
version "2.0.7"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
"@types/undertaker-registry@*":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/undertaker-registry/-/undertaker-registry-1.0.4.tgz#2ea4b68abd0b3ad6716ab8ac28734092c1d152c4"
@@ -3598,10 +3603,12 @@ domhandler@^4.2.0, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"
dompurify@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.0.tgz#c9c88390f024c2823332615c9e20a453cf3825dd"
integrity sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA==
dompurify@^3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6"
integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
domutils@^1.5.1:
version "1.7.0"