CVE-2023-38508: Missing Authorization (CWE-862)
value: Tuleap is an open source suite to improve management of software developments and collaboration. In Tuleap Community Edition prior to version 14.11.99.28 and Tuleap Enterprise Edition prior to versions 14.10-6 and 14.11-3, the preview of an artifact link with a type does not respect the project, tracker and artifact level permissions. The issue occurs on the artifact view (not reproducible on the artifact modal). Users might get access to information they should not have access to. Only the title, status, assigned to and last update date fields as defined by the semantics are impacted. If those fields have strict permissions (e.g. the title is only visible to a specific user group) those permissions are still enforced. Tuleap Community Edition 14.11.99.28, Tuleap Enterprise Edition 14.10-6, and Tuleap Enterprise Edition 14.11-3 contain a fix for this issue.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 4.3
Published: 2023-08-24T23:15Z
Repository: https://github.com/Enalean/tuleap
Language: PHP
Files changed: plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/ArtifactInTypeTablePresenter.php, plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/TypeTablePresenter.php, plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_ArtifactLink.php
Commit date: 2023-07-25 15:21:28 +0200
Commit message: Fixes request #33608: Preview of a linked artifact with a type does not respect permissions

Change-Id: I94bab99f318a79e91f42b5fb67a6c2d45075ccba

Diff: diff --git a/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/ArtifactInTypeTablePresenter.php b/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/ArtifactInTypeTablePresenter.php
index 73a1e12f8461..973ee16c8b0f 100644
--- a/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/ArtifactInTypeTablePresenter.php
+++ b/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/ArtifactInTypeTablePresenter.php
@@ -35,20 +35,20 @@ class ArtifactInTypeTablePresenter
     public $artifact_assignees;
     public $html_classes;
     public $artifactlink_field_id;
 
     public function __construct(
+        \PFUser $current_user,
         \Tuleap\Tracker\Artifact\Artifact $artifact,
         $html_classes,
         Tracker_FormElement_Field_ArtifactLink $field,
         public readonly bool $are_links_deletable,
     ) {
         $this->html_classes = $html_classes;
         $tracker            = $artifact->getTracker();
         $project            = $tracker->getProject();
         $user_helper        = \UserHelper::instance();
-        $current_user       = \UserManager::instance()->getCurrentUser();
 
         $this->direct_link_to_artifact   = $artifact->fetchDirectLinkToArtifact();
         $this->project_public_name       = $project->getPublicName();
         $this->tracker_name              = $this->emptyStringIfNull($tracker->getName());
         $this->artifact_id               = $artifact->getId();
diff --git a/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/TypeTablePresenter.php b/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/TypeTablePresenter.php
index 60a5b4a0df05..eff4850e302f 100644
--- a/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/TypeTablePresenter.php
+++ b/plugins/tracker/include/Tracker/FormElement/Field/ArtifactLink/Type/TypeTablePresenter.php
@@ -41,10 +41,11 @@ class TypeTablePresenter
     public $mass_unlink_title;
 
     public const TABLE_ID_PREFIX = "tracker_report_table_type_";
 
     public function __construct(
+        \PFUser $current_user,
         TypePresenter $type,
         array $artifact_links,
         bool $is_reverse_artifact_links,
         Tracker_FormElement_Field_ArtifactLink $field,
         public bool $are_links_deletable,
@@ -65,12 +66,16 @@ class TypeTablePresenter
 
         $art_factory          = \Tracker_ArtifactFactory::instance();
         $this->artifact_links = [];
         $html_classes         = '';
         foreach ($artifact_links as $artifact_link) {
-            $artifact               = $art_factory->getArtifactById($artifact_link->getArtifactId());
+            $artifact = $art_factory->getArtifactByIdUserCanView($current_user, $artifact_link->getArtifactId());
+            if ($artifact === null) {
+                continue;
+            }
             $this->artifact_links[] = new ArtifactInTypeTablePresenter(
+                $current_user,
                 $artifact,
                 $html_classes,
                 $field,
                 $this->are_links_deletable,
             );
@@ -78,15 +83,17 @@ class TypeTablePresenter
 
         $this->mass_unlink_title = dgettext('tuleap-tracker', 'Mark all links to be removed');
     }
 
     public static function buildForHeader(
+        \PFUser $current_user,
         TypePresenter $type_presenter,
         Tracker_FormElement_Field_ArtifactLink $field,
         bool $are_links_deletable,
     ): TypeTablePresenter {
         return new TypeTablePresenter(
+            $current_user,
             $type_presenter,
             [],
             false,
             $field,
             $are_links_deletable,
diff --git a/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_ArtifactLink.php b/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_ArtifactLink.php
index 8b6ac9aea34e..94d2564f0f73 100644
--- a/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_ArtifactLink.php
+++ b/plugins/tracker/include/Tracker/FormElement/Tracker_FormElement_Field_ArtifactLink.php
@@ -563,11 +563,11 @@ class Tracker_FormElement_Field_ArtifactLink extends Tracker_FormElement_Field /
                         class="tracker-form-element-artifactlink-renderer-async"
                         data-field-id="' . (int) $this->getId() . '"
                         data-renderer-data="' . Codendi_HTMLPurifier::instance()->purify($json_encoded_data) . '"></div></div>';
             }
 
-            $html .= $this->fetchTypeTables($artifact_links_to_render, $reverse_artifact_links);
+            $html .= $this->fetchTypeTables($current_user, $artifact_links_to_render, $reverse_artifact_links);
         } else {
             $html .= $this->getNoValueLabelForLinks($artifact);
         }
         $html .= '</div>';
 
@@ -607,11 +607,11 @@ class Tracker_FormElement_Field_ArtifactLink extends Tracker_FormElement_Field /
         $matching_ids = $artifact_links_per_tracker->getMatchingIDs();
 
         return $renderer->fetchAsArtifactLink($matching_ids, $this->getId(), $read_only, $prefill_removed_values, $prefill_edited_types, $reverse_artifact_links, false, $from_aid);
     }
 
-    private function fetchTypeTables(ArtifactLinksToRender $artifact_links_to_render, $is_reverse_artifact_links)
+    private function fetchTypeTables(\PFUser $current_user, ArtifactLinksToRender $artifact_links_to_render, $is_reverse_artifact_links): string
     {
         static $type_tables_cache = [];
         if (isset($type_tables_cache[spl_object_hash($artifact_links_to_render)][$is_reverse_artifact_links])) {
             return $type_tables_cache[spl_object_hash($artifact_links_to_render)][$is_reverse_artifact_links];
         }
@@ -619,10 +619,11 @@ class Tracker_FormElement_Field_ArtifactLink extends Tracker_FormElement_Field /
         $template_renderer = $this->getTemplateRenderer();
         foreach ($artifact_links_to_render->getArtifactLinksForPerTypeDisplay() as $artifact_links_per_type) {
             $html .= $template_renderer->renderToString(
                 'artifactlink-type-table',
                 new TypeTablePresenter(
+                    $current_user,
                     $artifact_links_per_type->getTypePresenter(),
                     $artifact_links_per_type->getArtifactLinks(),
                     $is_reverse_artifact_links,
                     $this,
                     $this->areLinksDeletable(
@@ -732,11 +733,11 @@ class Tracker_FormElement_Field_ArtifactLink extends Tracker_FormElement_Field /
                             }
                         }
                     }
                 }
 
-                $this->appendTypeTable($request, $result, $is_reverse);
+                $this->appendTypeTable($current_user, $request, $result, $is_reverse);
                 if ($result) {
                     $head = [];
                     $rows = [];
                     foreach ($result as $key => $value) {
                         $head[$key] = $value["head"];
@@ -1733,54 +1734,58 @@ class Tracker_FormElement_Field_ArtifactLink extends Tracker_FormElement_Field /
     private function getTemplateRenderer()
     {
         return TemplateRendererFactory::build()->getRenderer(TRACKER_TEMPLATE_DIR);
     }
 
-    private function appendTypeTable(Codendi_Request $request, array &$result, bool $is_reverse_artifact_links)
+    private function appendTypeTable(PFUser $current_user, Codendi_Request $request, array &$result, bool $is_reverse_artifact_links): void
     {
         if (! $this->getTracker()->isProjectAllowedToUseType()) {
             return;
         }
 
         $type_shortname = $request->get('type');
         if (! $type_shortname) {
             return;
         }
 
-        $type_presenter        = $this->getTypePresenterFactory()->getFromShortname($type_shortname);
+        $type_presenter = $this->getTypePresenterFactory()->getFromShortname($type_shortname);
+        if ($type_presenter === null) {
+            return;
+        }
         $key                   = "type_$type_shortname";
         $art_factory           = $this->getArtifactFactory();
         $artifact_html_classes = 'additional';
         $type_html             = '';
-        $head_html             = '';
         $ids                   = $request->get('ids');
+        $are_links_deletable   = $this->areLinksDeletable(
+            $type_presenter,
+            $is_reverse_artifact_links,
+        );
 
         foreach (explode(',', $ids) as $id) {
-            $artifact = $art_factory->getArtifactById(trim($id));
+            $artifact = $art_factory->getArtifactByIdUserCanView($current_user, (int) trim($id));
 
-            $are_links_deletable = $this->areLinksDeletable(
-                $type_presenter,
-                $is_reverse_artifact_links,
-            );
-
-            if (! is_null($artifact) && $artifact->getTracker()->isActive()) {
-                $type_html .= $this->getTemplateRenderer()->renderToString(
-                    'artifactlink-type-table-row',
-                    new ArtifactInTypeTablePresenter(
-                        $artifact,
-                        $artifact_html_classes,
-                        $this,
-                        $are_links_deletable,
-                    )
-                );
+            if ($artifact === null) {
+                continue;
             }
+
+            $type_html .= $this->getTemplateRenderer()->renderToString(
+                'artifactlink-type-table-row',
+                new ArtifactInTypeTablePresenter(
+                    $current_user,
+                    $artifact,
+                    $artifact_html_classes,
+                    $this,
+                    $are_links_deletable,
+                )
+            );
         }
 
         if ($type_html !== '') {
             $head_html = $this->getTemplateRenderer()->renderToString(
                 'artifactlink-type-table-head',
-                TypeTablePresenter::buildForHeader($type_presenter, $this, $are_links_deletable)
+                TypeTablePresenter::buildForHeader($current_user, $type_presenter, $this, $are_links_deletable)
             );
 
             $result[$key] = ['head' => $head_html, 'rows' => $type_html];
         } else {
             $result[$key] = [];

Vulnerable code:         $current_user       = \UserManager::instance()->getCurrentUser();
            $artifact               = $art_factory->getArtifactById($artifact_link->getArtifactId());
            $html .= $this->fetchTypeTables($artifact_links_to_render, $reverse_artifact_links);
    private function fetchTypeTables(ArtifactLinksToRender $artifact_links_to_render, $is_reverse_artifact_links)
                $this->appendTypeTable($request, $result, $is_reverse);
    private function appendTypeTable(Codendi_Request $request, array &$result, bool $is_reverse_artifact_links)
        $type_presenter        = $this->getTypePresenterFactory()->getFromShortname($type_shortname);
        $head_html             = '';
            $artifact = $art_factory->getArtifactById(trim($id));
            $are_links_deletable = $this->areLinksDeletable(
                $type_presenter,
                $is_reverse_artifact_links,
            );

            if (! is_null($artifact) && $artifact->getTracker()->isActive()) {
                $type_html .= $this->getTemplateRenderer()->renderToString(
                    'artifactlink-type-table-row',
                    new ArtifactInTypeTablePresenter(
                        $artifact,
                        $artifact_html_classes,
                        $this,
                        $are_links_deletable,
                    )
                );
                TypeTablePresenter::buildForHeader($type_presenter, $this, $are_links_deletable)
Fixed code:         \PFUser $current_user,
        \PFUser $current_user,
            $artifact = $art_factory->getArtifactByIdUserCanView($current_user, $artifact_link->getArtifactId());
            if ($artifact === null) {
                continue;
            }
                $current_user,
        \PFUser $current_user,
            $current_user,
            $html .= $this->fetchTypeTables($current_user, $artifact_links_to_render, $reverse_artifact_links);
    private function fetchTypeTables(\PFUser $current_user, ArtifactLinksToRender $artifact_links_to_render, $is_reverse_artifact_links): string
                    $current_user,
                $this->appendTypeTable($current_user, $request, $result, $is_reverse);
    private function appendTypeTable(PFUser $current_user, Codendi_Request $request, array &$result, bool $is_reverse_artifact_links): void
        $type_presenter = $this->getTypePresenterFactory()->getFromShortname($type_shortname);
        if ($type_presenter === null) {
            return;
        }
        $are_links_deletable   = $this->areLinksDeletable(
            $type_presenter,
            $is_reverse_artifact_links,
        );
            $artifact = $art_factory->getArtifactByIdUserCanView($current_user, (int) trim($id));
            if ($artifact === null) {
                continue;

            $type_html .= $this->getTemplateRenderer()->renderToString(
                'artifactlink-type-table-row',
                new ArtifactInTypeTablePresenter(
                    $current_user,
                    $artifact,
                    $artifact_html_classes,
                    $this,
                    $are_links_deletable,
                )
            );
                TypeTablePresenter::buildForHeader($current_user, $type_presenter, $this, $are_links_deletable)