From 73c06688773f48356d0f5076177d3aaeee6bc494 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 18 Aug 2026 14:36:50 +0200 Subject: [PATCH] :bug: Verify read access on source file in clone-file-media-object (#11090) The clone-file-media-object RPC command only checked edit permissions on the destination file. The source media object was fetched directly by UUID without verifying the caller had access to the file that owns it. This fix adds a read permission check on the source file before cloning. If the caller lacks read access to the source file, the operation fails with :not-found to avoid leaking information about the existence of files/media the caller cannot access. Closes #11087 AI-assisted-by: qwen3.7-plus --- backend/src/app/rpc/commands/media.clj | 7 +- backend/test/backend_tests/rpc_media_test.clj | 95 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index 418eeb5b47..4cf3c68029 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -272,8 +272,13 @@ (clone-file-media-object cfg params)) (defn clone-file-media-object - [{:keys [::db/conn]} {:keys [id file-id is-local]}] + [{:keys [::db/conn] :as cfg} {:keys [id file-id is-local] :as params}] (let [mobj (db/get-by-id conn :file-media-object id)] + (when-not mobj + (ex/raise :type :not-found + :code :object-not-found + :hint "source media object not found")) + (files/check-read-permissions! conn (::rpc/profile-id params) (:file-id mobj)) (db/insert! conn :file-media-object {:id (uuid/next) :file-id file-id diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index f7ac5dccf6..d230b03769 100644 --- a/backend/test/backend_tests/rpc_media_test.clj +++ b/backend/test/backend_tests/rpc_media_test.clj @@ -769,3 +769,98 @@ (t/is (some? (:error out))) (t/is (= :restriction (-> out :error ex-data :type))) (t/is (= :max-quote-reached (-> out :error ex-data :code))))))) + +;; --- Clone File Media Object BOLA tests --- + +(defn- create-storage-object! + [content content-type] + (let [storage (:app.storage/storage th/*system*)] + (sto/put-object! storage {::sto/content (sto/content content) + :content-type content-type}))) + +(t/deftest clone-file-media-object-success + (let [prof1 (th/create-profile* 1) + _ (th/create-project* 1 {:profile-id (:id prof1) + :team-id (:default-team-id prof1)}) + file1 (th/create-file* 1 {:profile-id (:id prof1) + :project-id (:default-project-id prof1) + :is-shared false}) + sobj (create-storage-object! "image-content" "image/png") + mobj (th/create-file-media-object* {:file-id (:id file1) + :name "test-media" + :width 100 + :height 100 + :mtype "image/png" + :media-id (:id sobj)}) + file2 (th/create-file* 2 {:profile-id (:id prof1) + :project-id (:default-project-id prof1) + :is-shared false}) + params {::th/type :clone-file-media-object + ::rpc/profile-id (:id prof1) + :file-id (:id file2) + :is-local true + :id (:id mobj)} + out (th/command! params)] + + (t/is (nil? (:error out))) + (let [result (:result out)] + (t/is (= (:id file2) (:file-id result))) + (t/is (= (:name mobj) (:name result))) + (t/is (= (:media-id mobj) (:media-id result))) + (t/is (uuid? (:id result))) + (t/is (not= (:id mobj) (:id result)))))) + +(t/deftest clone-file-media-object-no-read-access + (let [prof1 (th/create-profile* 1) + _ (th/create-project* 1 {:profile-id (:id prof1) + :team-id (:default-team-id prof1)}) + file1 (th/create-file* 1 {:profile-id (:id prof1) + :project-id (:default-project-id prof1) + :is-shared false}) + sobj (create-storage-object! "private-content" "image/png") + mobj (th/create-file-media-object* {:file-id (:id file1) + :name "private-media" + :width 100 + :height 100 + :mtype "image/png" + :media-id (:id sobj)}) + + prof2 (th/create-profile* 2) + _ (th/create-project* 2 {:profile-id (:id prof2) + :team-id (:default-team-id prof2)}) + file2 (th/create-file* 2 {:profile-id (:id prof2) + :project-id (:default-project-id prof2) + :is-shared false}) + + params {::th/type :clone-file-media-object + ::rpc/profile-id (:id prof2) + :file-id (:id file2) + :is-local true + :id (:id mobj)} + out (th/command! params)] + + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= :not-found (:type error-data))) + (t/is (= :object-not-found (:code error-data)))))) + +(t/deftest clone-file-media-object-source-not-found + (let [prof (th/create-profile* 1) + _ (th/create-project* 1 {:profile-id (:id prof) + :team-id (:default-team-id prof)}) + file (th/create-file* 1 {:profile-id (:id prof) + :project-id (:default-project-id prof) + :is-shared false}) + params {::th/type :clone-file-media-object + ::rpc/profile-id (:id prof) + :file-id (:id file) + :is-local true + :id (uuid/random)} + out (th/command! params)] + + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= :not-found (:type error-data))) + (t/is (= :object-not-found (:code error-data))))))