CVE-2022-31032 (PHP, published 2022-06-29T18:15Z)
Description: value: Tuleap is a Free & Open Source Suite to improve management of software developments and collaboration. In versions prior to 13.9.99.58 authorizations are not properly verified when creating projects or trackers from projects marked as templates. Users can get access to information in those template projects because the permissions model is not properly enforced. Users are advised to upgrade. There are no known workarounds for this issue.
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: NVD-CWE-Other \u2014 Other
NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Repository: https://github.com/Enalean/tuleap
Files changed:
plugins/program_management/tests/rest/v1/ProjectResourceTest.php
site-content/fr_FR/LC_MESSAGES/tuleap-core.po
site-content/pt_BR/LC_MESSAGES/tuleap-core.po
src/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilder.php
src/common/Project/REST/v1/ProjectResource.class.php
src/common/Project/Registration/Template/InsufficientPermissionToUseCompanyTemplateException.php
src/common/Project/Registration/Template/ProjectTemplateNotActiveException.php
src/common/Project/Registration/Template/TemplateFactory.php
src/common/Project/Registration/Template/TemplateFromProjectForCreation.php
tests/lib/TestDataBuilder.php
tests/rest/_fixtures/16-public-template/project.xml
tests/rest/_fixtures/16-public-template/user_map.csv
tests/rest/_fixtures/16-public-template/users.xml
tests/rest/_fixtures/17-private-template/project.xml
tests/rest/_fixtures/17-private-template/user_map.csv
tests/rest/_fixtures/17-private-template/users.xml
tests/rest/bin/init_data.php
tests/rest/bin/setup.sh
tests/rest/lib/RestBase.php
tests/rest/lib/TestDataBuilder.php
tests/rest/tests/ProjectTest.php
tests/rest/tests/ReadOnlyAdministrator/ProjectTest.php
tests/unit/common/Project/REST/v1/ProjectCreationDataPOSTProjectBuilderTest.php
tests/unit/common/Project/REST/v1/RestProjectCreatorTest.php
tests/unit/common/Project/Registration/Template/TemplateFactoryTest.php
tests/unit/common/Project/Registration/Template/TemplateFromProjectForCreationTest.php
Commit message: request #26816 Resources of private projects can be accessed by non project members

Authorizations are not properly verified when creating projects or
trackers from projects marked as templates : A classic user should not
be able create a project from a private template that he is not a
member.

Change-Id: Id8b599432923b32551379041a26d2acf0035a59d


Vulnerable code:
        $this->checkLinksArePresentInReleaseTopBacklog($team_id, [$user_story1['id']]);
msgid "Project #%d is not active and is not a template"
msgstr "Le project #%d n'est pas actif et n'est pas un modèle"
msgid "Project #%d is not active and is not a template"
msgstr "O projeto #%d não está ativo e não é um modelo"
                    $this->project_manager
                $this->getBackendLogger()
        ProjectAuthorization::userCanAccessProject($user, $project, new URLVerification());
        ProjectAuthorization::userCanAccessProject($user, $project, new URLVerification());
        parent::__construct('Project #' . $project->getID() . ' is not active and is not a template');
        return sprintf(_('Project #%d is not active and is not a template'), $this->project->getID());
            ScrumTemplate::NAME => new ScrumTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
            KanbanTemplate::NAME => new KanbanTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
            IssuesTemplate::NAME => new IssuesTemplate($glyph_finder, $consistency_checker, $event_dispatcher),
            EmptyTemplate::NAME => new EmptyTemplate($glyph_finder),
            $event_manager
            if ((int) $project_template->getGroupId() === \Project::ADMIN_PROJECT_ID) {
    private static function fromData(ProjectManager $project_manager, PFUser $user_requesting_creation, ?int $project_id): self
    {
        if (! $project->isActive() && ! $project->isTemplate()) {
        if (! $project->isTemplate() && ! $user_requesting_creation->isAdmin($project->getID())) {
            throw new InsufficientPermissionToUseProjectAsTemplateException($project, $user_requesting_creation);
        }
        return self::fromData($project_manager, $user_requesting_creation, $representation->template_id);
        $this->project_private_member_id         = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_MEMBER_SHORTNAME);
        $this->project_public_member_id          = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_MEMBER_SHORTNAME);
        $this->project_suspended_id              = $this->getProjectId(REST_TestDataBuilder::PROJECT_SUSPENDED_SHORTNAME);
        $this->project_public_with_membership_id = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_WITH_MEMBERSHIP_SHORTNAME);
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            REST_TestDataBuilder::TEST_USER_2_NAME,
            'template_id' => 100,
            'template_id' => 100,
            new NullLogger()
use Tuleap\Project\Registration\Template\Events\CollectCategorisedExternalTemplatesEvent;
        $this->creator        = new RestProjectCreator(
                new CollectCategorisedExternalTemplatesEvent()
     * @var \PHPUnit\Framework\MockObject\MockObject&ConsistencyChecker
     * @var \PHPUnit\Framework\MockObject\MockObject&\ProjectManager
    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100(): void
        $this->project_manager = $this->createMock(ProjectManager::class);
        $this->user            = $this->createMock(PFUser::class);
            $this->project_manager
        $project->method('isActive')->willReturn(false);
            $this->project_manager
    public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseAsTemplateIsNotActiveAndNotMarkedAsTemplate(): void
        $project->method('isTemplate')->willReturn(false);
            $this->project_manager
            $this->project_manager

Fixed code:
        $this->checkLinksArePresentInReleaseTopBacklog($release_mirror['id'], [$user_story1['id']]);
msgid "Project #%d is not active."
msgstr "Le project #%d n'est pas actif."
#, php-format
msgid ""
"Template %d is not valid: you don't have the permission to the access the "
"project."
msgstr ""
"Le modèle %d n'est pas valide : vous n'avez pas la permission d'accéder au "
"projet."

msgid "Project #%d is not active."
msgstr "O projeto #%d não está ativo."
#, php-format
msgid ""
"Template %d is not valid: you don't have the permission to the access the "
"project."
msgstr ""

use URLVerification;
        private URLVerification $url_verification,
                    $this->project_manager,
                    $this->url_verification
                $this->getBackendLogger(),
                $this->getURLVerification()
        ProjectAuthorization::userCanAccessProject($user, $project, $this->getURLVerification());
        ProjectAuthorization::userCanAccessProject($user, $project, $this->getURLVerification());

    private function getURLVerification(): URLVerification
    {
        return new URLVerification();
    }
<?php
/**
 * Copyright (c) Enalean, 2022-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/>.
 */

namespace Tuleap\Project\Registration\Template;

use Project;
use Project_Creation_Exception;

final class InsufficientPermissionToUseCompanyTemplateException extends Project_Creation_Exception implements InvalidTemplateException
{
    public function __construct(private Project $project)
    {
        parent::__construct(sprintf(_("Template %d is not valid: you don't have the permission to the access the project."), $this->project->getId()));
    }

    public function getI18NMessage(): string
    {
        return $this->message;
    }
}
        parent::__construct('Project #' . $project->getID() . ' is not active');
        return sprintf(_('Project #%d is not active.'), $this->project->getID());
use Project;
use Project_AccessException;
use URLVerification;
use UserManager;
        private UserManager $user_manager,
        private URLVerification $url_verification,
            ScrumTemplate::NAME    => new ScrumTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
            KanbanTemplate::NAME   => new KanbanTemplate($glyph_finder, $project_xml_merger, $consistency_checker),
            IssuesTemplate::NAME   => new IssuesTemplate($glyph_finder, $consistency_checker, $event_dispatcher),
            EmptyTemplate::NAME    => new EmptyTemplate($glyph_finder),
            $event_manager,
            \UserManager::instance(),
            new URLVerification()
            if ((int) $project_template->getGroupId() === \Project::ADMIN_PROJECT_ID || ! $this->userCanAccessTemplate($project_template)) {

    private function userCanAccessTemplate(Project $project_template): bool
    {
        try {
            return $this->url_verification->userCanAccessProject($this->user_manager->getCurrentUser(), $project_template);
        } catch (Project_AccessException $exception) {
            return false;
        }
    }
use Exception;
use URLVerification;
     * @throws InsufficientPermissionToUseCompanyTemplateException
    private static function fromData(
        ProjectManager $project_manager,
        PFUser $user_requesting_creation,
        ?int $project_id,
        URLVerification $url_verification,
    ): self {
        if (! self::doesProjectStatusAllowUsageAsTemplate($project)) {
        self::checkIfUserHasPermissionsToUseTemplate($project, $url_verification, $user_requesting_creation);
        URLVerification $url_verification,
        return self::fromData(
            $project_manager,
            $user_requesting_creation,
            $representation->template_id,
            $url_verification
        );
    private static function doesProjectStatusAllowUsageAsTemplate(Project $project): bool
    {
        return $project->isActive() || $project->isSystem();
    }

    /**
     * @throws InsufficientPermissionToUseCompanyTemplateException
     * @throws InsufficientPermissionToUseProjectAsTemplateException
     */
    private static function checkIfUserHasPermissionsToUseTemplate(
        Project $project,
        URLVerification $url_verification,
        PFUser $user_requesting_creation,
    ): void {
        if ($project->isTemplate()) {
            try {
                $url_verification->userCanAccessProject($user_requesting_creation, $project);
            } catch (Exception $exception) {
                throw new InsufficientPermissionToUseCompanyTemplateException($project);
            }
        } elseif (! $user_requesting_creation->isAdmin($project->getID())) {
            throw new InsufficientPermissionToUseProjectAsTemplateException($project, $user_requesting_creation);
        }
    }
    public const PROJECT_PUBLIC_TEMPLATE                  = 'public-template';
    public const PROJECT_PRIVATE_TEMPLATE                 = 'private-template';
<?xml version="1.0" encoding="UTF-8"?>
<project
        unix-name="public-template" full-name="Public Template" description="For template test" access="public">
    <long-description>A public template test project</long-description>
    <services>
    </services>
</project>
name,action,comment
<?xml version="1.0" encoding="UTF-8"?>
<users>
</users>
<?xml version="1.0" encoding="UTF-8"?>
<project
        unix-name="private-template" full-name="Private Template" description="For template test" access="private">
    <long-description>A private template test project</long-description>
    <ugroups>
        <ugroup name="project_members" description="">
            <members>
                <member format="username">rest_api_tester_3</member>
                <member format="username">rest_api_tester_5</member>
            </members>
        </ugroup>
        <ugroup name="project_admins" description="">
            <members>
                <member format="username">rest_api_tester_3</member>
            </members>
        </ugroup>
    </ugroups>
    <services>
    </services>
</project>
name,action,comment
<?xml version="1.0" encoding="UTF-8"?>
<users>
    <user>
        <id>104</id>
        <username>rest_api_tester_3</username>
        <realname><![CDATA[]]></realname>
        <email><![CDATA[test_user_3@example.com]]></email>
        <ldapid></ldapid>
    </user>
    <user>
        <id>105</id>
        <username>rest_api_tester_5</username>
        <realname><![CDATA[]]></realname>
        <email><![CDATA[test_user_5@example.com]]></email>
        <ldapid></ldapid>
    </user>
</users>
    ->markProjectsAsTemplate()
    load_project /usr/share/tuleap/tests/rest/_fixtures/16-public-template
    load_project /usr/share/tuleap/tests/rest/_fixtures/17-private-template
    protected $project_public_template_id;
    protected $project_private_template_id;
        $this->project_private_member_id         = $this->getProjectId(
            REST_TestDataBuilder::PROJECT_PRIVATE_MEMBER_SHORTNAME
        );
        $this->project_public_member_id          = $this->getProjectId(
            REST_TestDataBuilder::PROJECT_PUBLIC_MEMBER_SHORTNAME
        );
        $this->project_suspended_id              = $this->getProjectId(
            REST_TestDataBuilder::PROJECT_SUSPENDED_SHORTNAME
        );
        $this->project_public_with_membership_id = $this->getProjectId(
            REST_TestDataBuilder::PROJECT_PUBLIC_WITH_MEMBERSHIP_SHORTNAME
        );
        $this->project_public_template_id        = $this->getProjectId(REST_TestDataBuilder::PROJECT_PUBLIC_TEMPLATE);
        $this->project_private_template_id       = $this->getProjectId(REST_TestDataBuilder::PROJECT_PRIVATE_TEMPLATE);
use Tuleap\admin\ProjectEdit\ProjectEditDao;
    public function markProjectsAsTemplate()
    {
        echo "Mark public-template and private-template as template";

        $project_manager  = ProjectManager::instance();
        $public_template  = $project_manager->getProjectByUnixName("public-template");
        $private_template = $project_manager->getProjectByUnixName("private-template");

        $project_edit_dao = new ProjectEditDao();
        if ($public_template) {
            $project_edit_dao->updateProjectStatusAndType("A", TemplateSingleton::TEMPLATE, $public_template->getID());
        }

        if ($private_template) {
            $project_edit_dao->updateProjectStatusAndType("A", TemplateSingleton::TEMPLATE, $private_template->getID());
        }

        return $this;
    }

            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            'template_id' => $this->project_public_template_id,
            REST_TestDataBuilder::TEST_USER_5_NAME,
            REST_TestDataBuilder::TEST_USER_5_NAME,
        );
    }

    public function testPOSTForRegularUserWithTemplateProjectUserCantAccess()
    {
        $post_resource = json_encode([
                                         'label'       => 'Test from template without access',
                                         'shortname'   => 'template-no-access-user',
                                         'description' => 'Test project template for REST API Project Creation',
                                         'is_public'   => true,
                                         'template_id' => $this->project_private_template_id,
                                     ]);

        $response = $this->getResponseByName(
            $this->request_factory->createRequest(
                'POST',
                'projects'
            )->withBody(
                $this->stream_factory->createStream($post_resource)
            )
        );

        self::assertEquals(400, $response->getStatusCode());
    }

    public function testPOSTForRegularUserWithPrivateTemplateProjectUserCanAccess()
    {
        $post_resource = json_encode([
                                         'label'       => 'Test from private template with access',
                                         'shortname'   => 'template-private-user-access',
                                         'description' => 'Test project template for REST API Project Creation',
                                         'is_public'   => true,
                                         'template_id' => $this->project_private_template_id,
                                     ]);

        $response = $this->getResponseByName(
            REST_TestDataBuilder::TEST_USER_5_NAME,
            $this->request_factory->createRequest(
                'POST',
                'projects'
            )->withBody(
                $this->stream_factory->createStream($post_resource)
            )

        self::assertEquals(201, $response->getStatusCode());
            'template_id' => $this->project_public_template_id,
            'template_id' => $this->project_public_template_id,
            new NullLogger(),
            new \URLVerification()
use URLVerification;
use UserManager;

        $this->user_manager     = $this->createMock(UserManager::class);
        $this->url_verification = $this->createMock(URLVerification::class);

        $this->creator = new RestProjectCreator(
                $this->user_manager,
                $this->url_verification
use PHPUnit\Framework\MockObject\MockObject;
use Project_AccessPrivateException;
use Tuleap\Test\Builders\UserTestBuilder;
use URLVerification;
use UserManager;
     * @var MockObject&ConsistencyChecker
     * @var MockObject&\ProjectManager
    /**
     * @var UserManager&MockObject
     */
    private $user_manager;
    /**
     * @var URLVerification&MockObject
     */
    private $url_verification;
    private \PFUser $user;

        $this->user = UserTestBuilder::aUser()->build();

        $this->user_manager        = $this->createMock(UserManager::class);
        $this->url_verification    = $this->createMock(URLVerification::class);
            $this->user_manager,
            $this->url_verification
    public function testItReturnsCompanyTemplateWhenTheTemplateIdIsNot100AndUserCanAccess(): void
        $this->user_manager->method('getCurrentUser')->willReturn($this->user);
        $this->url_verification->method("userCanAccessProject")->with($this->user, $template100)->willReturn(true);


    public function testItDoesNotReturnsCompanyTemplateWhenTheUserCannotAccessIt(): void
    {
        $this->consistency_checker->method('areAllServicesAvailable')->willReturn(true);

        $template110 = $this->createMock(\Project::class);
        $template110->expects(self::once())->method('getGroupId')->willReturn("110");
        $template110->method('getUnixNameLowerCase')->willReturn("hustler-company");
        $template110->method('getDescription')->willReturn("New Jack City");
        $template110->method('getPublicName')->willReturn("Hustler Company");

        $this->project_manager->method('getSiteTemplates')->willReturn([$template110]);

        $this->user_manager->method('getCurrentUser')->willReturn($this->user);
        $this->url_verification->method("userCanAccessProject")->with($this->user, $template110)->willThrowException(new Project_AccessPrivateException());

        self::assertEmpty($this->factory->getCompanyTemplateList());
    }
use PHPUnit\Framework\MockObject\Stub;
use URLVerification;
    /**
     * @var URLVerification&Stub
     */
    private $url_verification;
        $this->project_manager  = $this->createMock(ProjectManager::class);
        $this->user             = $this->createMock(PFUser::class);
        $this->url_verification = $this->createStub(URLVerification::class);
            $this->project_manager,
            $this->url_verification
        $project->method('isActive')->willReturn(true);
        $this->url_verification->method('userCanAccessProject')->willReturn(true);

            $this->project_manager,
            $this->url_verification
    public function testGetTemplateFromProjectForCreationIsNotValidWhenProjectToUseAsTemplateIsNotActive(): void
        $project->method('isSystem')->willReturn(false);
            $this->project_manager,
            $this->url_verification
            $this->project_manager,
            $this->url_verification
        );
    }

    public function testGetTemplateFromProjectForCreationIsNotValidWhenTemplateProjectToUseIsActiveButTheUserCanAccessTheProject(): void
    {
        $representation = ProjectPostRepresentation::build(125);

        $project = $this->createMock(Project::class);
        $project->method('getID')->willReturn($representation->template_id);
        $project->method('isError')->willReturn(false);
        $project->method('isActive')->willReturn(true);
        $project->method('isTemplate')->willReturn(true);

        $this->url_verification->method('userCanAccessProject')->willReturn(false);

        $this->project_manager->method('getProject')->with($representation->template_id)->willThrowException(new InsufficientPermissionToUseCompanyTemplateException($project));

        $this->expectException(InsufficientPermissionToUseCompanyTemplateException::class);
        TemplateFromProjectForCreation::fromRESTRepresentation(
            $representation,
            $this->user,
            $this->project_manager,
            $this->url_verification