CVE-2024-30246 (published 2024-03-29T16:15Z)

Tuleap is an Open Source Suite to improve management of software developments and collaboration. A malicious user could exploit this issue on purpose to delete information on the instance or possibly gain access to restricted artifacts. It is however not possible to control exactly which information is deleted. Information from theDate, File, Float, Int, List, OpenList, Text, and Permissions on artifact (this one can lead to the disclosure of restricted information) fields can be impacted.  This vulnerability is fixed in Tuleap Community Edition version 15.7.99.6 and Tuleap Enterprise Edition 15.7-2, 15.6-5, 15.5-6, 15.4-8, 15.3-6, 15.2-5, 15.1-9, 15.0-9, and 14.12-6.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/Enalean/tuleap
commit_details: repo_url: https://github.com/Enalean/tuleap
author: Thomas Gerbet
author_date: 2024-03-28 16:33:26+01:00
msg: fix: request #37545 Deleting or moving an artifact can delete values from unrelated artifacts

Issue has been introduced by git #tuleap/stable/7f2e5e974596196bd96c2146c533353ecbcc592f.

Change-Id: I04ac31d1803abcd5ce46f121e1411825f063247b
repository: repo_url: https://github.com/Enalean/tuleap
repo_name: Enalean/tuleap
description: Tuleap is an Open Source Suite to improve management of software developments and collaboration. With a single web-based solution, project managers, developers & quality managers can easily build, deploy software projects.
repo_language: PHP
owner: Enalean
file_changes: filename: ArchiveAndDeleteArtifactTaskBuilder.php
change_type: ModificationType.MODIFY
diff: @@ -217,7 +217,6 @@ class ArchiveAndDeleteArtifactTaskBuilder
                 new ComputedFieldDaoCache(new ComputedFieldDao()),
                 new RecentlyVisitedDao(),
                 new PendingArtifactRemovalDao(),
-                new ArtifactChangesetValueDeletorDAO(),
                 new PostArtifactMoveReferencesCleaner(
                     $cross_reference_manager,
                     new ReverseLinksRetriever(new ReverseLinksDao(), $tracker_artifact_factory),

programming_language: PHP
filename: ArtifactChangesetValueDeletorDAO.php
change_type: ModificationType.DELETE
diff: @@ -1,61 +0,0 @@
-<?php
-/**
- * Copyright (c) Enalean 2023 - Present. All Rights Reserved.
- *
- * This file is a part of Tuleap.
- *
- * Tuleap is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * Tuleap is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
- */
-
-declare(strict_types=1);
-
-namespace Tuleap\Tracker\Artifact\ArtifactsDeletion;
-
-use Tuleap\DB\DataAccessObject;
-use Tuleap\Tracker\Artifact\Artifact;
-
-class ArtifactChangesetValueDeletorDAO extends DataAccessObject
-{
-    public function cleanAllChangesetValueInTransaction(Artifact $artifact): void
-    {
-        $sql  = 'SELECT cvalue.changeset_id, changeset.id FROM tracker_changeset AS changeset
-                INNER JOIN tracker_changeset_value AS cvalue ON changeset.id = cvalue.changeset_id WHERE artifact_id = ?';
-        $rows = $this->getDB()->run($sql, $artifact->getId());
-
-        foreach ($rows as $row) {
-            $this->getDB()->delete('tracker_changeset_value_artifactlink', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_computedfield_manual_value', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_date', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_file', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_float', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_int', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_list', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_openlist', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_permissionsonartifact', ['changeset_value_id' => $row['changeset_id']]);
-            $this->getDB()->delete('tracker_changeset_value_text', ['changeset_value_id' => $row['changeset_id']]);
-
-            $sql = 'DELETE tracker_changeset_comment, tracker_changeset_comment_fulltext
-                    FROM tracker_changeset_comment
-                    INNER JOIN tracker_changeset_comment_fulltext ON tracker_changeset_comment.id = tracker_changeset_comment_fulltext.comment_id
-                    WHERE changeset_id = ?';
-            $this->getDB()->run($sql, $row['changeset_id']);
-
-            $this->getDB()->delete('tracker_changeset_incomingmail', ['changeset_id' => $row['id']]);
-
-            $this->getDB()->delete('tracker_changeset_value', ['changeset_id' => $row['id']]);
-        }
-
-        $this->getDB()->delete('tracker_changeset', ['artifact_id' => $artifact->getId()]);
-    }
-}

programming_language: unknown
filename: ArtifactDependenciesCleaner.php
change_type: ModificationType.MODIFY
diff: @@ -36,7 +36,6 @@ class ArtifactDependenciesCleaner
         private readonly ComputedFieldDaoCache $computed_dao_cache,
         private readonly RecentlyVisitedDao $recently_visited_dao,
         private readonly PendingArtifactRemovalDao $artifact_removal,
-        private readonly ArtifactChangesetValueDeletorDAO $changeset_value_deletor_DAO,
         private readonly PostArtifactMoveReferencesCleaner $post_artifact_move_references_cleaner,
         private readonly PostArtifactDeletionCleaner $artifact_deletion_cleaner,
     ) {
@@ -48,7 +47,6 @@ class ArtifactDependenciesCleaner
 
         $this->cleanReferences($artifact, $context, $user);
 
-        $this->changeset_value_deletor_DAO->cleanAllChangesetValueInTransaction($artifact);
         $this->dao->deleteUnsubscribeNotificationForArtifact($artifact->getId());
         // We do not keep trace of the history change here because it doesn't have any sense
         $this->tracker_artifact_priority_manager->deletePriority($artifact);

programming_language: PHP
filename: PendingArtifactRemovalDao.php
change_type: ModificationType.MODIFY
diff: @@ -41,10 +41,35 @@ class PendingArtifactRemovalDao extends DataAccessObject
         return $this->getDB()->row($sql, $artifact_id);
     }
 
-    public function removeArtifact($artifact_id)
+    public function removeArtifact($artifact_id): void
     {
-        $sql = 'DELETE FROM plugin_tracker_artifact_pending_removal
-                  WHERE id = ?';
+        $tables_to_clean_with_join = [
+            'tracker_changeset' => 'tracker_changeset.artifact_id = plugin_tracker_artifact_pending_removal.id',
+            'tracker_changeset_comment' => 'tracker_changeset_comment.changeset_id = tracker_changeset.id',
+            'tracker_changeset_comment_fulltext' => 'tracker_changeset_comment_fulltext.comment_id = tracker_changeset_comment.id',
+            'tracker_changeset_incomingmail' => 'tracker_changeset_incomingmail.changeset_id = tracker_changeset.id',
+            'tracker_changeset_value' => 'tracker_changeset_value.changeset_id = tracker_changeset.id',
+            'tracker_changeset_value_artifactlink' => 'tracker_changeset_value_artifactlink.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_computedfield_manual_value' => 'tracker_changeset_value_computedfield_manual_value.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_date' => 'tracker_changeset_value_date.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_file' => 'tracker_changeset_value_file.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_float' => 'tracker_changeset_value_float.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_int' => 'tracker_changeset_value_int.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_list' => 'tracker_changeset_value_list.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_openlist' => 'tracker_changeset_value_openlist.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_permissionsonartifact' => 'tracker_changeset_value_permissionsonartifact.changeset_value_id = tracker_changeset_value.id',
+            'tracker_changeset_value_text' => 'tracker_changeset_value_text.changeset_value_id = tracker_changeset_value.id',
+        ];
+
+        $tables = \Psl\Str\join(array_keys($tables_to_clean_with_join), ', ');
+        $joins  = '';
+        foreach ($tables_to_clean_with_join as $table => $join) {
+            $joins = $joins . sprintf('LEFT JOIN %s ON (%s) ', $table, $join);
+        }
+
+        $sql = "DELETE plugin_tracker_artifact_pending_removal, $tables
+                FROM plugin_tracker_artifact_pending_removal $joins
+                WHERE plugin_tracker_artifact_pending_removal.id = ?";
 
         $this->getDB()->run($sql, $artifact_id);
     }

programming_language: PHP
filename: ArtifactDependenciesDeletorTest.php
change_type: ModificationType.MODIFY
diff: @@ -52,7 +52,6 @@ final class ArtifactDependenciesDeletorTest extends TestCase
         $computed_dao_cache                = $this->createStub(ComputedFieldDaoCache::class);
         $recently_visited_dao              = $this->createStub(RecentlyVisitedDao::class);
         $artifact_removal                  = $this->createStub(PendingArtifactRemovalDao::class);
-        $value_deletor                     = $this->createStub(ArtifactChangesetValueDeletorDAO::class);
         $this->post_move_deletor           = $this->createStub(PostArtifactMoveReferencesCleaner::class);
         $this->artifact_deletor            = $this->createStub(PostArtifactDeletionCleaner::class);
 
@@ -62,7 +61,6 @@ final class ArtifactDependenciesDeletorTest extends TestCase
         $computed_dao_cache->expects(self::atLeastOnce())->method('deleteAllArtifactCacheValues');
         $recently_visited_dao->expects(self::atLeastOnce())->method('deleteVisitByArtifactId');
         $artifact_removal->expects(self::atLeastOnce())->method('removeArtifact');
-        $value_deletor->expects(self::atLeastOnce())->method('cleanAllChangesetValueInTransaction');
 
         $this->deletor = new ArtifactDependenciesCleaner(
             $permissions_manager,
@@ -71,7 +69,6 @@ final class ArtifactDependenciesDeletorTest extends TestCase
             $computed_dao_cache,
             $recently_visited_dao,
             $artifact_removal,
-            $value_deletor,
             $this->post_move_deletor,
             $this->artifact_deletor
         );

programming_language: PHP