CVE-2024-30246 (PHP, published 2024-03-29T16:15Z)
Description: 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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/Enalean/tuleap
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 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


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 = ?";