mirror of
https://github.com/penpot/penpot.git
synced 2026-08-06 12:58:55 +00:00
🐛 Add bounding box dimension limit to prevent export DoS
Add max-export-dimension constant (100000 units) and validate in calculate-dimensions. Reject exports when bounding box width, height, or position exceeds the limit to prevent resource exhaustion in the Chromium export pool. AI-assisted-by: mimo-v2.5-pro
This commit is contained in:
parent
319a2185c9
commit
746aaf3a0b
@ -15,6 +15,7 @@
|
|||||||
["react-dom/server" :as rds]
|
["react-dom/server" :as rds]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.rect :as grc]
|
[app.common.geom.rect :as grc]
|
||||||
@ -59,6 +60,7 @@
|
|||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
(def ^:const viewbox-decimal-precision 3)
|
(def ^:const viewbox-decimal-precision 3)
|
||||||
|
(def ^:const max-export-dimension 100000)
|
||||||
(def ^:private default-color clr/canvas)
|
(def ^:private default-color clr/canvas)
|
||||||
|
|
||||||
(mf/defc background
|
(mf/defc background
|
||||||
@ -82,12 +84,20 @@
|
|||||||
(let [bounds
|
(let [bounds
|
||||||
(->> root-objects
|
(->> root-objects
|
||||||
(map (partial gsb/get-object-bounds objects))
|
(map (partial gsb/get-object-bounds objects))
|
||||||
(grc/join-rects))]
|
(grc/join-rects))
|
||||||
|
bounds (-> bounds
|
||||||
|
(update :x mth/finite 0)
|
||||||
|
(update :y mth/finite 0)
|
||||||
|
(update :width mth/finite 100000)
|
||||||
|
(update :height mth/finite 100000))]
|
||||||
|
(when (or (> (:width bounds) max-export-dimension)
|
||||||
|
(> (:height bounds) max-export-dimension)
|
||||||
|
(> (+ (:x bounds) (:width bounds)) max-export-dimension)
|
||||||
|
(> (+ (:y bounds) (:height bounds)) max-export-dimension))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :export-area-too-large
|
||||||
|
:hint "export area exceeds maximum allowed dimensions"))
|
||||||
(-> bounds
|
(-> bounds
|
||||||
(update :x mth/finite 0)
|
|
||||||
(update :y mth/finite 0)
|
|
||||||
(update :width mth/finite 100000)
|
|
||||||
(update :height mth/finite 100000)
|
|
||||||
(grc/update-rect :position)
|
(grc/update-rect :position)
|
||||||
(grc/fix-aspect-ratio aspect-ratio))))))
|
(grc/fix-aspect-ratio aspect-ratio))))))
|
||||||
|
|
||||||
|
|||||||
78
frontend/test/frontend_tests/render_dimensions_test.cljs
Normal file
78
frontend/test/frontend_tests/render_dimensions_test.cljs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
;; 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 Sucursal en España SL
|
||||||
|
|
||||||
|
(ns frontend-tests.render-dimensions-test
|
||||||
|
(:require
|
||||||
|
[app.common.geom.rect :as grc]
|
||||||
|
[app.common.geom.shapes.bounds :as gsb]
|
||||||
|
[app.common.test-helpers.files :as cthf]
|
||||||
|
[app.common.test-helpers.ids-map :as cthi]
|
||||||
|
[app.common.test-helpers.shapes :as cths]
|
||||||
|
[app.common.types.shape :as cts]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.render :as render]
|
||||||
|
[cljs.test :as t :include-macros true]))
|
||||||
|
|
||||||
|
(defn- make-objects
|
||||||
|
"Create a proper objects map with a root frame and the given shapes."
|
||||||
|
[& shapes]
|
||||||
|
(let [root-frame (cts/setup-shape {:id uuid/zero
|
||||||
|
:type :frame
|
||||||
|
:parent-id uuid/zero
|
||||||
|
:frame-id uuid/zero
|
||||||
|
:name "Root Frame"
|
||||||
|
:shapes (mapv :id shapes)})
|
||||||
|
objects {uuid/zero root-frame}]
|
||||||
|
(reduce (fn [objs shape]
|
||||||
|
(assoc objs (:id shape) (assoc shape :frame-id uuid/zero)))
|
||||||
|
objects
|
||||||
|
shapes)))
|
||||||
|
|
||||||
|
(t/deftest calculate-dimensions-normal-bounds
|
||||||
|
(t/testing "Normal bounding box should pass"
|
||||||
|
(let [shape1 (cts/setup-shape {:type :rect :x 100 :y 100 :width 200 :height 150})
|
||||||
|
shape2 (cts/setup-shape {:type :rect :x 400 :y 300 :width 100 :height 100})
|
||||||
|
objects (make-objects shape1 shape2)
|
||||||
|
result (render/calculate-dimensions objects nil)]
|
||||||
|
(t/is (some? result))
|
||||||
|
(t/is (<= (:width result) render/max-export-dimension))
|
||||||
|
(t/is (<= (:height result) render/max-export-dimension)))))
|
||||||
|
|
||||||
|
(t/deftest calculate-dimensions-extreme-width
|
||||||
|
(t/testing "Extreme width should throw export-area-too-large"
|
||||||
|
(let [shape (cts/setup-shape {:type :rect :x 0 :y 0 :width 200000 :height 100})
|
||||||
|
objects (make-objects shape)]
|
||||||
|
(t/is (thrown-with-msg?
|
||||||
|
js/Error
|
||||||
|
#"export area exceeds maximum allowed dimensions"
|
||||||
|
(render/calculate-dimensions objects nil))))))
|
||||||
|
|
||||||
|
(t/deftest calculate-dimensions-extreme-height
|
||||||
|
(t/testing "Extreme height should throw export-area-too-large"
|
||||||
|
(let [shape (cts/setup-shape {:type :rect :x 0 :y 0 :width 100 :height 200000})
|
||||||
|
objects (make-objects shape)]
|
||||||
|
(t/is (thrown-with-msg?
|
||||||
|
js/Error
|
||||||
|
#"export area exceeds maximum allowed dimensions"
|
||||||
|
(render/calculate-dimensions objects nil))))))
|
||||||
|
|
||||||
|
(t/deftest calculate-dimensions-extreme-position
|
||||||
|
(t/testing "Shape at extreme position should throw export-area-too-large"
|
||||||
|
(let [shape (cts/setup-shape {:type :rect :x 500000 :y 500000 :width 100 :height 100})
|
||||||
|
objects (make-objects shape)]
|
||||||
|
(t/is (thrown-with-msg?
|
||||||
|
js/Error
|
||||||
|
#"export area exceeds maximum allowed dimensions"
|
||||||
|
(render/calculate-dimensions objects nil))))))
|
||||||
|
|
||||||
|
(t/deftest calculate-dimensions-exactly-at-limit
|
||||||
|
(t/testing "Bounding box exactly at limit should pass"
|
||||||
|
(let [shape (cts/setup-shape {:type :rect :x 0 :y 0 :width render/max-export-dimension :height render/max-export-dimension})
|
||||||
|
objects (make-objects shape)
|
||||||
|
result (render/calculate-dimensions objects nil)]
|
||||||
|
(t/is (some? result))
|
||||||
|
(t/is (<= (:width result) render/max-export-dimension))
|
||||||
|
(t/is (<= (:height result) render/max-export-dimension)))))
|
||||||
@ -51,6 +51,7 @@
|
|||||||
[frontend-tests.plugins.tokens-test]
|
[frontend-tests.plugins.tokens-test]
|
||||||
[frontend-tests.plugins.utils-test]
|
[frontend-tests.plugins.utils-test]
|
||||||
[frontend-tests.plugins.value-objects-test]
|
[frontend-tests.plugins.value-objects-test]
|
||||||
|
[frontend-tests.render-dimensions-test]
|
||||||
[frontend-tests.render-wasm.process-objects-test]
|
[frontend-tests.render-wasm.process-objects-test]
|
||||||
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
[frontend-tests.render-wasm.text-editor-caret-color-test]
|
||||||
[frontend-tests.svg-fills-test]
|
[frontend-tests.svg-fills-test]
|
||||||
@ -160,6 +161,7 @@
|
|||||||
'frontend-tests.ui.gradient-handlers-test
|
'frontend-tests.ui.gradient-handlers-test
|
||||||
'frontend-tests.ui.layout-container-multiple-test
|
'frontend-tests.ui.layout-container-multiple-test
|
||||||
'frontend-tests.ui.measures-menu-props-test
|
'frontend-tests.ui.measures-menu-props-test
|
||||||
|
'frontend-tests.render-dimensions-test
|
||||||
'frontend-tests.text-editor-paste-guard-test
|
'frontend-tests.text-editor-paste-guard-test
|
||||||
'frontend-tests.ui.settings-password-schema-test
|
'frontend-tests.ui.settings-password-schema-test
|
||||||
'frontend-tests.ui.settings-shortcuts-test
|
'frontend-tests.ui.settings-shortcuts-test
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user