🐛 Scope assemble-chunks session lookup to profile-id

Prevent BOLA in chunked upload assembly by verifying session
ownership. The assemble-chunks function now requires a profile-id
parameter and scopes the upload_session lookup accordingly, matching
the pattern already used by upload-chunk.

All three callers (assemble-file-media-object, create-font-variant,
import-binfile) updated to pass the authenticated profile-id.

AI-assisted-by: mimo-v2.5-pro
This commit is contained in:
Andrey Antukh 2026-07-28 09:06:41 +00:00
parent 319a2185c9
commit 77033af34e
4 changed files with 42 additions and 6 deletions

View File

@ -162,7 +162,7 @@
params
(if (some? upload-id)
(let [file (db/tx-run! cfg media-cmd/assemble-chunks upload-id)]
(let [file (db/tx-run! cfg media-cmd/assemble-chunks profile-id upload-id)]
(assoc params :file file))
params)

View File

@ -113,10 +113,10 @@
"Assembles each chunked-upload session in `uploads` (a `{mtype
session-id}` map) into a temp file, validates the media type and
size of every entry, and returns a `{mtype path}` data map."
[cfg {:keys [uploads] :as params}]
[cfg {:keys [::rpc/profile-id uploads] :as params}]
(let [data (reduce-kv
(fn [acc mtype session-id]
(let [assembled (assemble-chunks cfg session-id)]
(let [assembled (assemble-chunks cfg profile-id session-id)]
(-> {:mtype mtype :size (:size assembled)}
(media/validate-media-type! cm/font-types)
(media/validate-font-size!))

View File

@ -391,9 +391,10 @@
Raises a :validation/:missing-chunks error when the number of stored
chunks does not match `:total-chunks` recorded in the session row.
Raises :not-found when the session does not belong to `profile-id`.
Deletes the session row from `upload_session` on success."
[{:keys [::db/conn] :as cfg} session-id]
(let [session (db/get conn :upload-session {:id session-id})
[{:keys [::db/conn] :as cfg} profile-id session-id]
(let [session (db/get conn :upload-session {:id session-id :profile-id profile-id})
chunks (get-upload-chunks conn session-id)]
(when (not= (count chunks) (:total-chunks session))
@ -436,7 +437,7 @@
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(let [content (assemble-chunks cfg session-id)
(let [content (assemble-chunks cfg profile-id session-id)
content (-> content
(assoc :filename (str "upload:" name))
(assoc :mtype mtype)

View File

@ -548,6 +548,41 @@
(t/is (some? (:error out)))
(t/is (= :not-found (-> out :error ex-data :type)))))
(t/deftest chunked-upload-other-profile-cannot-assemble
;; assemble-chunks must scope the session lookup to the requesting
;; profile so that a different profile cannot assemble chunks from
;; a session they do not own (BOLA / CWE-639).
(let [prof1 (th/create-profile* 1)
prof2 (th/create-profile* 2)
session-id (create-session! prof1 1)
source-path (th/tempfile "backend_tests/test_files/sample.jpg")
mfile {:filename "sample.jpg"
:path source-path
:mtype "image/jpeg"
:size 312043}]
;; prof1 uploads a chunk into their own session
(let [out (th/command! {::th/type :upload-chunk
::rpc/profile-id (:id prof1)
:session-id session-id
:index 0
:content mfile})]
(t/is (nil? (:error out))))
;; prof2 tries to assemble prof1's session via create-font-variant
;; (which calls assemble-chunks without ownership check)
(let [out (th/command! {::th/type :create-font-variant
::rpc/profile-id (:id prof2)
:team-id (:default-team-id prof2)
:font-id (uuid/next)
:font-family "TestFont"
:font-weight 400
:font-style "normal"
:uploads {"font/ttf" session-id}})]
(t/is (some? (:error out)))
(t/is (= :not-found (-> out :error ex-data :type)))
(t/is (= :object-not-found (-> out :error ex-data :code))))))
(t/deftest chunked-upload-invalid-media-type
(let [prof (th/create-profile* 1)
_ (th/create-project* 1 {:profile-id (:id prof)