From afe42a4cb5d7a965ee0dfa1d1e7a8dcfa040b4a9 Mon Sep 17 00:00:00 2001 From: Mega Yu Date: Thu, 12 Mar 2026 15:26:59 +0800 Subject: [PATCH] chore(security): upgrade dompurify and unify sanitizer path --- package.json | 2 +- .../frontend/components/plugins_settings.cljs | 10 +--- src/main/frontend/security.cljs | 34 +++++++++++++- src/test/frontend/security_test.cljs | 47 +++++++++++++++++++ yarn.lock | 15 ++++-- 5 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 src/test/frontend/security_test.cljs diff --git a/package.json b/package.json index 46bfcac6af..940485cb4c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/frontend/components/plugins_settings.cljs b/src/main/frontend/components/plugins_settings.cljs index 130fb606b9..1a47570096 100644 --- a/src/main/frontend/components/plugins_settings.cljs +++ b/src/main/frontend/components/plugins_settings.cljs @@ -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!]}] diff --git a/src/main/frontend/security.cljs b/src/main/frontend/security.cljs index 0c7f9d0690..cb7ae2d781 100644 --- a/src/main/frontend/security.cljs +++ b/src/main/frontend/security.cljs @@ -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)) diff --git a/src/test/frontend/security_test.cljs b/src/test/frontend/security_test.cljs new file mode 100644 index 0000000000..eed5da6502 --- /dev/null +++ b/src/test/frontend/security_test.cljs @@ -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 "

safe

" + fake-purify #js {:sanitize (fn [input opts] + (reset! called {:input input + :opts (js->clj opts)}) + "

safe

")}] + (with-redefs [security/get-dompurify (fn [] fake-purify)] + (is (= "

safe

" + (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 [_ _] "

safe

")}] + (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)))))) diff --git a/yarn.lock b/yarn.lock index 21848b8a1d..705a78bd8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"