CVE-2021-41154 (PHP, published 2021-10-18T22:15Z)
Description: value: Tuleap is a Free & Open Source Suite to improve management of software developments and collaboration. In affected versions an attacker with read access to a "SVN core" repository could execute arbitrary SQL queries. The following versions contain the fix: Tuleap Community Edition 11.17.99.144, Tuleap Enterprise Edition 11.17-5, Tuleap Enterprise Edition 11.16-7.
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-89 \u2014 Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
Repository: https://github.com/Enalean/tuleap
Files changed:
src/common/Widget/Widget_MyLatestSvnCommits.class.php
src/common/Widget/Widget_ProjectLatestSvnCommits.class.php
src/common/svn/SVN_LogFactory.class.php
src/www/svn/browse_revision.php
src/www/svn/svn_utils.php
tests/psalm/tuleap-baseline.xml
Commit message: request #16213: SQL injection in the "SVN core" commits browser

Also fixes a XSS via the same injection point.

Change-Id: Ib33199b7d0ec5f8a07a65d74766477f23df4a75b


Vulnerable code:
 * Copyright (c) Enalean, 2013 - 2018. All Rights Reserved.
                    list($latest_revisions, $nb_revisions) = svn_get_revisions($project, 0, $this->_nb_svn_commits, '', $user->getUserName(), '', '', 0, false);
                list($this->latest_revisions,) = svn_get_revisions($project, 0, 5, '', '', '', '', 0, false);
 * Copyright (c) Enalean, 2012-2018. All Rights Reserved.
            '',
    $order_by = '';
            $order_by = ' ORDER BY ' . svn_utils_criteria_list_to_query($morder);
// Transform criteria list to SQL query (+ means ascending
// - is descending)
function svn_utils_criteria_list_to_query($criteria_list)
    $criteria_list = str_replace('>', ' ASC', $criteria_list);
    $criteria_list = str_replace('<', ' DESC', $criteria_list);
    return $criteria_list;
            $arr_text[] = '<a href="' . $url . '&morder=' . $morder . '#results">' .
            svn_utils_field_get_label($attr) . '</a><img src="' . util_get_dir_image_theme() .
function svn_get_revisions(Project $project, $offset, $chunksz, $_rev_id = '', $_commiter = '', $_srch = '', $order_by = '', $pv = 0, $foundRows = true)
    // SQLi Warning: no real possibility to escape $order_by here.
    // We rely on a proper filtering of user input by calling methods.
    if (! isset($order_by) || $order_by == '') {
        $order_by = " ORDER BY revision DESC ";
    }

    $sql = $select . $from . $where . $group_by . $order_by . $limit;
        if ($result1 && ! db_error($result1)) {
    <DeprecatedFunction occurrences="20">

Fixed code:
 * Copyright (c) Enalean, 2013 - Present. All Rights Reserved.
                    list($latest_revisions, $nb_revisions) = svn_get_revisions($project, 0, $this->_nb_svn_commits, '', $user->getUserName(), '', [], 0, false);
                list($this->latest_revisions,) = svn_get_revisions($project, 0, 5, '', '', '', [], 0, false);
 * Copyright (c) Enalean, 2012-Present. All Rights Reserved.
            [],
    $order_by = [];
            $order_by = svn_utils_criteria_list_to_query($morder);
/**
 * @psalm-return array<array{order:"ASC"|"DESC", column: "revision"|"commit_id"|"description"|"date"|"whoid"}>
 */
function svn_utils_criteria_list_to_query(string $criteria_list): array
    $order_list = [];

    foreach (explode(',', $criteria_list) as $criteria) {
        if (preg_match('/^(?<column>[a-z]+)(?<order>[<>]?)$/', $criteria, $matches) === 1) {
            $column = $matches['column'];
            if (! in_array($column, ['revision', 'commit_id', 'description', 'date', 'whoid'], true)) {
                continue;
            }

            $order = 'ASC';
            if ($matches['order'] === '<') {
                $order = 'DESC';
            }

            $order_list[] = ['order' => $order, 'column' => $column];
        }
    }

    return $order_list;
        $purifier = Codendi_HTMLPurifier::instance();

            $arr_text[] = '<a href="' . $url . '&morder=' . $purifier->purify(urlencode($morder)) . '#results">' .
            $purifier->purify(svn_utils_field_get_label($attr)) . '</a><img src="' . util_get_dir_image_theme() .
/**
 * @psalm-param array<array{order:"ASC"|"DESC", column: "revision"|"commit_id"|"description"|"date"|"whoid"}> $order_by
 */
function svn_get_revisions(Project $project, $offset, $chunksz, $_rev_id = '', $_commiter = '', $_srch = '', array $order_by = [], $pv = 0, $foundRows = true)
    $limit = '';
    if (empty($order_by)) {
        $order_by_sql = " ORDER BY revision DESC ";
    } else {
        $order_by_sql = ' ORDER BY ';
        $order_by_sql .= implode(
            ',',
            array_map(
                static function (array $order_by_row) {
                    return $order_by_row['column'] . ' ' . $order_by_row['order'];
                },
                $order_by
            )
        );
        $order_by_sql .= ' ';
    }

    $sql = $select . $from . $where . $group_by . $order_by_sql . $limit;
        if ($result1 && ! db_error()) {
    <DeprecatedFunction occurrences="25">
      <code>db_query($sql)</code>
      <code>db_query($sql1)</code>
      <code>db_fetch_array($sql1)</code>
      <code>db_error()</code>