CVE-2024-30246: Insufficient Information (NVD-CWE-noinfo)
value: 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.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-29T16:15Z
Repository: https://github.com/Enalean/tuleap
Language: PHP
Files changed: plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArchiveAndDeleteArtifactTaskBuilder.php, plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesCleaner.php, plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/PendingArtifactRemovalDao.php, plugins/tracker/tests/unit/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesDeletorTest.php
Commit date: 2024-03-28 17:34:09 +0100
Commit message: 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

Diff: diff --git a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArchiveAndDeleteArtifactTaskBuilder.php b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArchiveAndDeleteArtifactTaskBuilder.php
index e112f7c332cc0..06470477089dd 100644
--- a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArchiveAndDeleteArtifactTaskBuilder.php
+++ b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArchiveAndDeleteArtifactTaskBuilder.php
@@ -215,11 +215,10 @@ class ArchiveAndDeleteArtifactTaskBuilder
                 ),
                 $tracker_artifact_dao,
                 new ComputedFieldDaoCache(new ComputedFieldDao()),
                 new RecentlyVisitedDao(),
                 new PendingArtifactRemovalDao(),
-                new ArtifactChangesetValueDeletorDAO(),
                 new PostArtifactMoveReferencesCleaner(
                     $cross_reference_manager,
                     new ReverseLinksRetriever(new ReverseLinksDao(), $tracker_artifact_factory),
                     $artifact_linker,
                     $tracker_artifact_factory
diff --git a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactChangesetValueDeletorDAO.php b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactChangesetValueDeletorDAO.php
deleted file mode 100644
index 964f2616f8a1c..0000000000000
--- a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactChangesetValueDeletorDAO.php
+++ /dev/null
@@ -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()]);
-    }
-}
diff --git a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesCleaner.php b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesCleaner.php
index d78fc8bfa7253..f34f6889a8f45 100644
--- a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesCleaner.php
+++ b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesCleaner.php
@@ -34,11 +34,10 @@ class ArtifactDependenciesCleaner
         private readonly Tracker_Artifact_PriorityManager $tracker_artifact_priority_manager,
         private readonly Tracker_ArtifactDao $dao,
         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,
     ) {
     }
 
@@ -46,11 +45,10 @@ class ArtifactDependenciesCleaner
     {
         $this->permissions_manager->clearPermission(Artifact::PERMISSION_ACCESS, (string) $artifact->getId());
 
         $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);
         $this->computed_dao_cache->deleteAllArtifactCacheValues($artifact);
         $this->recently_visited_dao->deleteVisitByArtifactId($artifact->getId());
diff --git a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/PendingArtifactRemovalDao.php b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/PendingArtifactRemovalDao.php
index fba30c56f2091..3a0b08ca753cb 100644
--- a/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/PendingArtifactRemovalDao.php
+++ b/plugins/tracker/include/Tracker/Artifact/ArtifactsDeletion/PendingArtifactRemovalDao.php
@@ -39,13 +39,38 @@ class PendingArtifactRemovalDao extends DataAccessObject
                   WHERE id = ?';
 
         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);
     }
 }
diff --git a/plugins/tracker/tests/unit/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesDeletorTest.php b/plugins/tracker/tests/unit/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesDeletorTest.php
index c2510d0509629..ecb00d1f1ab35 100644
--- a/plugins/tracker/tests/unit/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesDeletorTest.php
+++ b/plugins/tracker/tests/unit/Tracker/Artifact/ArtifactsDeletion/ArtifactDependenciesDeletorTest.php
@@ -50,30 +50,27 @@ final class ArtifactDependenciesDeletorTest extends TestCase
         $tracker_artifact_priority_manager = $this->createStub(Tracker_Artifact_PriorityManager::class);
         $dao                               = $this->createStub(Tracker_ArtifactDao::class);
         $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);
 
         $permissions_manager->expects(self::atLeastOnce())->method('clearPermission');
         $dao->expects(self::atLeastOnce())->method('deleteUnsubscribeNotificationForArtifact');
         $tracker_artifact_priority_manager->expects(self::atLeastOnce())->method('deletePriority');
         $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,
             $tracker_artifact_priority_manager,
             $dao,
             $computed_dao_cache,
             $recently_visited_dao,
             $artifact_removal,
-            $value_deletor,
             $this->post_move_deletor,
             $this->artifact_deletor
         );
 
         $this->tracker = TrackerTestBuilder::aTracker()->build();

Vulnerable code:                 new ArtifactChangesetValueDeletorDAO(),
<?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()]);
    }
}
        private readonly ArtifactChangesetValueDeletorDAO $changeset_value_deletor_DAO,
        $this->changeset_value_deletor_DAO->cleanAllChangesetValueInTransaction($artifact);
    public function removeArtifact($artifact_id)
        $sql = 'DELETE FROM plugin_tracker_artifact_pending_removal
                  WHERE id = ?';
        $value_deletor                     = $this->createStub(ArtifactChangesetValueDeletorDAO::class);
        $value_deletor->expects(self::atLeastOnce())->method('cleanAllChangesetValueInTransaction');
            $value_deletor,
Fixed code:     public function removeArtifact($artifact_id): void
        $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 = ?";