🐛 Verify read access on source file in clone-file-media-object

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
This commit is contained in:
Andrey Antukh 2026-08-05 10:49:52 +00:00
parent 636bc22cc4
commit cff9939552
2 changed files with 101 additions and 1 deletions

View File

@ -262,8 +262,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

View File

@ -734,3 +734,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))))))