diff --git a/backend/src/app/binfile/v3.clj b/backend/src/app/binfile/v3.clj index 952cb69e8f..a5e69223c5 100644 --- a/backend/src/app/binfile/v3.clj +++ b/backend/src/app/binfile/v3.clj @@ -856,6 +856,15 @@ :expected-size (:size object) :found-size (sto/get-size content))) + (when-let [max (::bfc/import-max-object-size cfg)] + (when (> (sto/get-size content) max) + (ex/raise :type :validation + :code :max-file-size-reached + :hint (str "storage object exceeds maximum size: " (sto/get-size content)) + :path path + :max max + :found (sto/get-size content)))) + (when-let [hash (get object :hash)] (when (not= hash (sto/get-hash content)) (ex/raise :type :validation @@ -938,6 +947,15 @@ (let [manifest (-> (read-manifest input) (validate-manifest)) entries (read-zip-entries input) + + _ (when-let [max (::bfc/import-max-zip-entries cfg)] + (when (> (count entries) max) + (ex/raise :type :validation + :code :too-many-zip-entries + :hint (str "zip file has too many entries: " (count entries)) + :max max + :found (count entries)))) + cfg (-> cfg (assoc ::entries entries) (assoc ::manifest manifest) diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 27ca224a88..66b52b7aab 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -94,7 +94,11 @@ ;; SSRF protection :ssrf-allowed-hosts #{} - :ssrf-extra-blocked-cidrs #{}}) + :ssrf-extra-blocked-cidrs #{} + + ;; Binfile import limits + :binfile-import-max-object-size (* 1024 1024 100) ;; 100 MiB + :binfile-import-max-zip-entries (* 500 1000)}) ;; 500,000 (def schema:config (do #_sm/optional-keys @@ -147,6 +151,10 @@ [:imagemagick-width-limit {:optional true} :string] [:imagemagick-height-limit {:optional true} :string] + ;; Binfile import limits (PENPOT_BINFILE_IMPORT_*) + [:binfile-import-max-object-size {:optional true} ::sm/int] + [:binfile-import-max-zip-entries {:optional true} ::sm/int] + [:deletion-delay {:optional true} ::ct/duration] [:file-clean-delay {:optional true} ::ct/duration] [:telemetry-enabled {:optional true} ::sm/boolean] diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index a900d52203..313391e76a 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -92,7 +92,9 @@ (assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team)) (assoc ::bfc/project-id project-id) (assoc ::bfc/profile-id profile-id) - (assoc ::bfc/name name)) + (assoc ::bfc/name name) + (assoc ::bfc/import-max-object-size (cf/get :binfile-import-max-object-size)) + (assoc ::bfc/import-max-zip-entries (cf/get :binfile-import-max-zip-entries))) input-path (:path file) owned? (some? upload-id) diff --git a/backend/test/backend_tests/binfile_test.clj b/backend/test/backend_tests/binfile_test.clj index 17993b38c3..9343b6ff5b 100644 --- a/backend/test/backend_tests/binfile_test.clj +++ b/backend/test/backend_tests/binfile_test.clj @@ -132,3 +132,31 @@ ;; With the guard, it raises :validation :max-file-size-reached. (t/is (= :validation (:type out))) (t/is (= :max-file-size-reached (:code out)))))))) + +(t/deftest import-rejects-too-many-zip-entries + ;; N1-09: import must reject ZIP files exceeding max-zip-entries + (let [profile (th/create-profile* 1) + file (prepare-simple-file profile) + output (tmp/tempfile :suffix ".zip")] + + (v3/export-files! + (-> th/*system* + (assoc ::bfc/ids #{(:id file)}) + (assoc ::bfc/embed-assets false) + (assoc ::bfc/include-libraries false)) + (io/output-stream output)) + + ;; Import with max-zip-entries=1 — the exported ZIP has more entries + (let [cfg (-> th/*system* + (assoc ::bfc/project-id (:default-project-id profile)) + (assoc ::bfc/profile-id (:id profile)) + (assoc ::bfc/input output) + (assoc ::bfc/import-max-zip-entries 1)) + out (try + (v3/import-files! cfg) + :no-error + (catch Throwable e + (let [d (or (ex-data e) (some-> (ex-cause e) ex-data))] + d)))] + (t/is (= :validation (:type out))) + (t/is (= :too-many-zip-entries (:code out))))))