mirror of
https://github.com/penpot/penpot.git
synced 2026-05-16 05:23:39 +00:00
🐛 Prevent CSS injection vulnerability in font family names
Add a shared `schema:font-family` whitelist validator in app.common.types.font that only allows letters, digits, spaces, hyphens, underscores, and dots in font family names. Apply the schema to create-font-variant and update-font RPC endpoints on the backend, and add client-side validation in the dashboard fonts UI. Include unit tests for the schema and integration tests for the RPC handlers. Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
29f940fb7a
commit
67d9567971
@ -13,6 +13,7 @@
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.font :as types.font]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
@ -96,7 +97,7 @@
|
||||
[:map {:title "create-font-variant"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:font-id ::sm/uuid]
|
||||
[:font-family ::sm/text]
|
||||
[:font-family types.font/schema:font-family]
|
||||
[:font-weight [::sm/one-of {:format "number"} valid-weight]]
|
||||
[:font-style [::sm/one-of {:format "string"} valid-style]]
|
||||
[:data {:optional true} [:map-of ::sm/text [:or ::sm/bytes [::sm/vec ::sm/bytes]]]]
|
||||
@ -273,7 +274,7 @@
|
||||
[:map {:title "update-font"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]])
|
||||
[:name types.font/schema:font-family]])
|
||||
|
||||
(sv/defmethod ::update-font
|
||||
{::doc/added "1.18"
|
||||
|
||||
@ -790,3 +790,100 @@
|
||||
(t/is (some? (:error out)))
|
||||
(t/is (= :validation (-> out :error ex-data :type)))
|
||||
(t/is (= :media-type-not-allowed (-> out :error ex-data :code))))))
|
||||
|
||||
;; --- Font family name validation / XSS prevention
|
||||
|
||||
(t/deftest create-font-variant-with-invalid-family
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
font-id (uuid/custom 10 100)
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf") (io/read*))]
|
||||
|
||||
;; name with < should fail
|
||||
(let [params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :font-id font-id
|
||||
:font-family "evil<script>alert(1)</script>"
|
||||
:font-weight 400 :font-style "normal"
|
||||
:data {"font/ttf" data}}
|
||||
out (th/command! params)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
||||
|
||||
;; name with ' should fail
|
||||
(let [params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :font-id font-id
|
||||
:font-family "evil'name"
|
||||
:font-weight 400 :font-style "normal"
|
||||
:data {"font/ttf" data}}
|
||||
out (th/command! params)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation)))
|
||||
|
||||
;; name with } should fail
|
||||
(let [params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :font-id font-id
|
||||
:font-family "evil}name"
|
||||
:font-weight 400 :font-style "normal"
|
||||
:data {"font/ttf" data}}
|
||||
out (th/command! params)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation)))
|
||||
|
||||
;; valid name should succeed
|
||||
(let [params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :font-id (uuid/custom 10 101)
|
||||
:font-family "Source Sans Pro"
|
||||
:font-weight 400 :font-style "normal"
|
||||
:data {"font/ttf" data}}
|
||||
out (th/command! params)]
|
||||
(t/is (th/success? out))))))
|
||||
|
||||
(t/deftest update-font-with-invalid-family
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
font-id (uuid/custom 10 102)
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf") (io/read*))]
|
||||
|
||||
;; Create a valid font first
|
||||
(let [params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :font-id font-id
|
||||
:font-family "ValidFont"
|
||||
:font-weight 400 :font-style "normal"
|
||||
:data {"font/ttf" data}}
|
||||
out (th/command! params)]
|
||||
(t/is (th/success? out)))
|
||||
|
||||
;; rename with < should fail
|
||||
(let [params {::th/type :update-font
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :id font-id
|
||||
:name "evil<script>x</script>"}
|
||||
out (th/command! params)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation))
|
||||
(t/is (th/ex-of-code? (:error out) :params-validation)))
|
||||
|
||||
;; rename with ' should fail
|
||||
(let [params {::th/type :update-font
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :id font-id
|
||||
:name "evil'name"}
|
||||
out (th/command! params)]
|
||||
(t/is (not (th/success? out)))
|
||||
(t/is (th/ex-of-type? (:error out) :validation)))
|
||||
|
||||
;; valid rename should succeed
|
||||
(let [params {::th/type :update-font
|
||||
::rpc/profile-id (:id prof)
|
||||
:team-id team-id :id font-id
|
||||
:name "Valid Font Name"}
|
||||
out (th/command! params)]
|
||||
(t/is (th/success? out))))))
|
||||
|
||||
21
common/src/app/common/types/font.cljc
Normal file
21
common/src/app/common/types/font.cljc
Normal file
@ -0,0 +1,21 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.types.font
|
||||
(:require
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
(def ^:private font-family-re
|
||||
;; \p{L} (Unicode letter) works in Java regex natively, but in JavaScript it
|
||||
;; requires the "u" flag which ClojureScript regex literals don't support.
|
||||
#?(:clj #"[\p{L}\d _.-]+"
|
||||
:cljs (js/RegExp. "[\\p{L}\\d _.-]+" "u")))
|
||||
|
||||
(def schema:font-family
|
||||
[:and
|
||||
[::sm/text {:max 250}]
|
||||
[:fn {:error/code "errors.font-family-invalid-chars"}
|
||||
(fn [s] (boolean (re-matches font-family-re s)))]])
|
||||
41
common/test/common_tests/types/font_test.cljc
Normal file
41
common/test/common_tests/types/font_test.cljc
Normal file
@ -0,0 +1,41 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.types.font-test
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.font :as ctf]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/deftest font-family-schema-valid
|
||||
(t/is (sm/validate ctf/schema:font-family "Source Sans Pro"))
|
||||
(t/is (sm/validate ctf/schema:font-family "Roboto"))
|
||||
(t/is (sm/validate ctf/schema:font-family "Open Sans 300"))
|
||||
(t/is (sm/validate ctf/schema:font-family "Font-Name_v2"))
|
||||
(t/is (sm/validate ctf/schema:font-family "Noto Sans CJK SC"))
|
||||
(t/is (sm/validate ctf/schema:font-family "A"))
|
||||
;; hyphens, underscores and dots are allowed
|
||||
(t/is (sm/validate ctf/schema:font-family "Fira-Code"))
|
||||
(t/is (sm/validate ctf/schema:font-family "font_name"))
|
||||
(t/is (sm/validate ctf/schema:font-family "Soucre Sans Pro 3.0"))
|
||||
;; Unicode letters are allowed
|
||||
(t/is (sm/validate ctf/schema:font-family "思源黑体"))
|
||||
(t/is (sm/validate ctf/schema:font-family "العربية")))
|
||||
|
||||
(t/deftest font-family-schema-invalid
|
||||
;; HTML injection characters
|
||||
(t/is (not (sm/validate ctf/schema:font-family "evil<script>")))
|
||||
(t/is (not (sm/validate ctf/schema:font-family "<test>name")))
|
||||
;; CSS injection characters
|
||||
(t/is (not (sm/validate ctf/schema:font-family "evil'name")))
|
||||
(t/is (not (sm/validate ctf/schema:font-family "evil\"name")))
|
||||
(t/is (not (sm/validate ctf/schema:font-family "evil}name")))
|
||||
(t/is (not (sm/validate ctf/schema:font-family "evil;name")))
|
||||
(t/is (not (sm/validate ctf/schema:font-family "evil\\name")))
|
||||
;; empty string
|
||||
(t/is (not (sm/validate ctf/schema:font-family "")))
|
||||
;; too long
|
||||
(t/is (not (sm/validate ctf/schema:font-family (apply str (repeat 251 "a"))))))
|
||||
@ -9,6 +9,8 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.media :as cm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.font :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.fonts :as df]
|
||||
@ -139,7 +141,8 @@
|
||||
(dom/get-data "id")
|
||||
(uuid/parse))
|
||||
name (dom/get-value target)]
|
||||
(when-not (str/blank? name)
|
||||
(when (and (not (str/blank? name))
|
||||
(sm/validate ctf/schema:font-family name))
|
||||
(swap! fonts* df/rename-and-regroup id name installed-fonts)))))
|
||||
|
||||
on-change-name
|
||||
@ -317,7 +320,9 @@
|
||||
(fn [_]
|
||||
(reset! edition* false)
|
||||
(when-not (str/blank? font-family)
|
||||
(st/emit! (df/update-font {:id font-id :name font-family})))))
|
||||
(if (sm/validate ctf/schema:font-family font-family)
|
||||
(st/emit! (df/update-font {:id font-id :name font-family}))
|
||||
(st/emit! (ntf/error (tr "errors.font-family-invalid-chars")))))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
|
||||
@ -1439,6 +1439,10 @@ msgstr "Invalid text"
|
||||
msgid "errors.team-name-invalid-chars"
|
||||
msgstr "The team name can't contain any of the following characters:'.', ':' or '/'"
|
||||
|
||||
#: common/src/app/common/types/font.cljc
|
||||
msgid "errors.font-family-invalid-chars"
|
||||
msgstr "The font family name can only contain letters, numbers, spaces, hyphens, underscores, and dots."
|
||||
|
||||
#: src/app/main/ui/static.cljs:74
|
||||
msgid "errors.invite-invalid"
|
||||
msgstr "Invite invalid"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user