From fb0727389791a92303d6fffaba0cf05b3aa400dc Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 5 Aug 2026 17:40:04 +0200 Subject: [PATCH] :bug: Validate library belongs to same team in link/unlink/sync handlers (#11016) Add check-library-team-ownership! helper that verifies both the file and library share the same team before creating or modifying library relations. This prevents cross-team library injection where a user with edit permissions on files in different teams could link them across team boundaries. Applied to link-file-to-library, unlink-file-from-library, and update-file-library-sync-status handlers. AI-assisted-by: mimo-v2.5 --- backend/src/app/rpc/commands/files.clj | 22 ++++++++++++++ backend/test/backend_tests/rpc_file_test.clj | 32 ++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index e10c85a7bd..69bead539d 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1069,6 +1069,25 @@ [cfg {:keys [::rpc/profile-id] :as params}] (db/tx-run! cfg delete-file (assoc params :profile-id profile-id))) +;; --- Library relation helpers + +(defn- check-library-team-ownership! + "Verify that file and library belong to the same team. + Prevents cross-team library relation injection." + [conn file-id library-id] + (let [sql "SELECT EXISTS ( + SELECT 1 FROM file AS f + JOIN project AS fp ON (fp.id = f.project_id) + JOIN file AS l ON (l.id = ?) + JOIN project AS lp ON (lp.id = l.project_id) + WHERE f.id = ? AND fp.team_id = lp.team_id + ) AS ok" + row (db/exec-one! conn [sql library-id file-id])] + (when-not (:ok row) + (ex/raise :type :not-found + :code :object-not-found + :hint "file and library must belong to the same team")))) + ;; --- MUTATION COMMAND: link-file-to-library (def sql:link-file-to-library @@ -1104,6 +1123,7 @@ (check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id library-id) + (check-library-team-ownership! conn file-id library-id) (let [transitive-deps (bfc/get-libraries cfg [library-id])] (when (contains? transitive-deps file-id) @@ -1135,6 +1155,7 @@ [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}] (check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id library-id) + (check-library-team-ownership! conn file-id library-id) (unlink-file-from-library conn params) nil) @@ -1159,6 +1180,7 @@ [{:keys [::db/conn]} {:keys [::rpc/profile-id file-id library-id] :as params}] (check-edition-permissions! conn profile-id file-id) (check-edition-permissions! conn profile-id library-id) + (check-library-team-ownership! conn file-id library-id) (update-sync conn params)) ;; --- MUTATION COMMAND: ignore-sync diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index 1c07f35971..18ad14e639 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -983,6 +983,38 @@ (t/is (some? sync)) (t/is (some? (:synced-at sync))))) +(t/deftest link-file-to-library-rejects-cross-team + ;; N1-08: A file in team2 must not be linked to a library in team1, + ;; even when the user has edit permissions on both (BOLA / CWE-639). + (let [prof1 (th/create-profile* 1) + prof2 (th/create-profile* 2) + team1 (th/create-team* 1 {:profile-id (:id prof1)}) + team2 (th/create-team* 2 {:profile-id (:id prof2)}) + proj1 (th/create-project* 1 {:profile-id (:id prof1) + :team-id (:id team1)}) + proj2 (th/create-project* 2 {:profile-id (:id prof2) + :team-id (:id team2)}) + lib (th/create-file* 1 {:project-id (:id proj1) + :profile-id (:id prof1) + :is-shared true}) + file2 (th/create-file* 2 {:project-id (:id proj2) + :profile-id (:id prof2)})] + + ;; Add prof2 as editor to team1 so they have edit access to the library + (th/db-insert! :team-profile-rel {:team-id (:id team1) + :profile-id (:id prof2) + :is-owner false + :is-admin false + :can-edit true}) + + ;; prof2 tries to link file2 (team2) to lib (team1) — must fail + (let [data {::th/type :link-file-to-library + ::rpc/profile-id (:id prof2) + :file-id (:id file2) + :library-id (:id lib)} + out (th/command! data)] + (t/is (some? (:error out)))))) + (t/deftest update-file-library-sync-status-updates-sync-row (let [profile (th/create-profile* 1) file1 (th/create-file* 1 {:project-id (:default-project-id profile)