CVE-2023-35929 (JavaScript, published 2023-07-25T18:15Z)
Description: value: Tuleap is a free and open source suite to improve management of software development and collaboration. Prior to version 14.10.99.4 of Tuleap Community Edition and prior to versions 14.10-2 and 14.9-5 of Tuleap Enterprise Edition, content displayed in the "card fields" (visible in the kanban and PV2 apps) is not properly escaped. A malicious user with the capability to create an artifact or to edit a field used as a card field could force victim to execute uncontrolled code. Tuleap Community Edition 14.10.99.4, Tuleap Enterprise Edition 14.10-2, and Tuleap Enterprise Edition 14.9-5 contain a fix.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/Enalean/tuleap
Files changed:
plugins/agiledashboard/scripts/lib/card-fields/package.json
plugins/agiledashboard/scripts/lib/card-fields/pnpm-lock.yaml
plugins/agiledashboard/scripts/lib/card-fields/src/card-fields-service.js
plugins/agiledashboard/scripts/lib/card-fields/src/card-fields.tpl.html
plugins/agiledashboard/scripts/lib/card-fields/src/highlight-filter-template.test.ts
plugins/agiledashboard/scripts/lib/card-fields/src/highlight-filter-template.ts
plugins/agiledashboard/scripts/lib/card-fields/src/highlight-filter.js
plugins/agiledashboard/scripts/lib/card-fields/src/index.js
plugins/agiledashboard/scripts/lib/card-fields/vite.config.ts
Commit message: Fixes request #32629: XSS in the card field of the agile dashboard apps

Tried to keep the changes within the scope of the security issue. The
template engine of hybrids is bit abused but it is still better than
before. A future contribution might want to consider removing angular
completely from @tuleap/plugin-agiledashboard-card-fields.

This change also fixes the display of the legacy colors for selectbox
using them.

Change-Id: I301ed6a72c324054a490605e881646bf317316fb


Vulnerable code:
    "escape-string-regexp": "^2.0.0"
    "angular-sanitize": "^1.8.0",
    "angular-mocks": "^1.8.2",
    "angular-sanitize": "^1.8.2"
  angular-sanitize:
    specifier: ^1.8.2
    version: 1.8.2
  /angular-sanitize@1.8.2:
    resolution: {integrity: sha512-OB6Goa+QN3byf5asQ7XRl7DKZejm/F/ZOqa9z1skqYVOWA2hoBxoCmt9E7+i7T/TbxZP5zYzKxNZVVJNu860Hg==}
    dev: true

import moment from "moment";
CardFieldsService.$inject = ["$sce", "$filter"];

function CardFieldsService($sce, $filter) {
    const highlight = $filter("tuleapHighlight");
        getCardFieldDateValue,
                return getCardFieldUserValue(value, filter_terms);
            return highlight(value.label, filter_terms);
            const color = $sce.getTrustedHtml(`<span class="extra-card-field-color"
                style="background: rgb(${r}, ${g}, ${b})"></span>`);
            return color + highlight(value.label, filter_terms);
            const color = $sce.getTrustedHtml(
                `<span class="extra-card-field-color card-field-${tlp_color}"></span>`
            );
            return color + highlight(label, filter_terms);
        return $sce.trustAsHtml(values.map(getValueRendered).join(", "));
    }

    function getCardFieldDateValue(value) {
        return moment(value).fromNow();
            var file_name = highlight(file.name, filter_terms);

            return (
                '<a data-nodrag="true" href="' +
                getFileUrl(file) +
                '" title="' +
                file.description +
                '"><i class="fas fa-paperclip extra-card-field-file-icon"></i>' +
                file_name +
                "</a>"
            );
        return file_descriptions.map(getFileLink).join(", ");
    function getCardFieldPermissionsValue(values) {
        return values.join(", ");
    function getCardFieldUserValue(value, filter_terms) {
        let display_name;

            display_name = highlight(value.display_name, filter_terms);
            return `<div class="tlp-avatar-mini"> </div><span>${display_name}</span>`;
        display_name = highlight(value.display_name, filter_terms);
        return `<a data-nodrag="true" class="extra-card-field-user" href="${value.user_url}">
                            <div class="tlp-avatar-mini"><img src="${value.avatar_url}" /></div><span>${display_name}</span>
                        </a>`;
            <span ng-bind-html="card_ctrl.getCardFieldPermissionsValue(card_field.granted_groups) | tuleapHighlight:card_ctrl.filter_terms"></span>
import { isNumber } from "angular";
import { Classifier } from "./highlight/Classifier";
import { HighlightedText } from "./highlight/HighlightedText";
    function isTextSearchable(text, search) {
        return text && (search || isNumber(search));
        if (!isTextSearchable(text, search)) {
            return text ? text.toString() : text;

        const classifier = Classifier(String(search));
        const parts = classifier.classify(String(text)).map((highlighted_text) => {
            if (!HighlightedText.isHighlight(highlighted_text)) {
                return highlighted_text.content;
            }
            return `<span class="highlight">${highlighted_text.content}</span>`;
        });
        return parts.join("");
import ngSanitize from "angular-sanitize";
    .module("card-fields", [ngSanitize])
            external: ["angular", "angular-sanitize", "moment"],
                    "angular-sanitize": "angular-sanitize",
                    moment: "moment",

Fixed code:
    "escape-string-regexp": "^2.0.0",
    "hybrids": "^8.2.1"
    "angular-mocks": "^1.8.2"
  hybrids:
    specifier: ^8.2.1
    version: 8.2.1

  /hybrids@8.2.1:
    resolution: {integrity: sha512-ZIjDLZi6aBOL+Cdry1xtBLJJnMzx99HNwmOCQXEvqdawyYu949z469vxgExyAgq0CA4lSfJsjh1rsiIsGdqXdw==}
    engines: {node: '>=18.0.0'}
    hasBin: true
    dev: false
import { html } from "hybrids";
import { highlightFilterElements } from "./highlight-filter-template";
CardFieldsService.$inject = ["$sce"];
function CardFieldsService($sce) {
                return getCardFieldUserValueTemplate(value, filter_terms);
            return highlightFilterElements(value.label, filter_terms);
            const styles = {
                background: `rgb(${r}, ${g}, ${b})`,
            };

            return html`<span class="extra-card-field-color" style="${styles}"></span
                >${highlightFilterElements(value.label, filter_terms)}`;
            const classlist = ["extra-card-field-color", `card-field-${tlp_color}`];
            return html`<span class="${classlist}"></span>${highlightFilterElements(
                    label,
                    filter_terms
                )}`;
        return getHTMLStringFromTemplate(renderListItems(values, getValueRendered));
            const file_display = html`<i class="fas fa-paperclip extra-card-field-file-icon"></i
                >${highlightFilterElements(file.name, filter_terms)}`;
            return html`<a data-nodrag="true" href="${getFileUrl(file)}" title="${file.description}"
                >${file_display}</a
            >`;
        return getHTMLStringFromTemplate(renderListItems(file_descriptions, getFileLink));
    function getCardFieldPermissionsValue(values, filter_terms) {
        return getHTMLStringFromTemplate(
            renderListItems(
                values,
                (value) => html`${highlightFilterElements(value, filter_terms)}`
            )
        );
    function getCardFieldUserValueTemplate(value, filter_terms) {
        const display_name = highlightFilterElements(value.display_name, filter_terms);
            return html`<div class="tlp-avatar-mini"></div>
                <span>${display_name}</span>`;
        return html`<a data-nodrag="true" class="extra-card-field-user" href="${value.user_url}">
            <div class="tlp-avatar-mini"><img loading="lazy" src="${value.avatar_url}" /></div>
            <span>${display_name}</span>
        </a>`;
    }

    function getCardFieldUserValue(value, filter_terms) {
        return getHTMLStringFromTemplate(getCardFieldUserValueTemplate(value, filter_terms));

    function renderListItems(items, render_item) {
        let templated_content = html``;

        for (const [i, file] of items.entries()) {
            if (i === 0) {
                templated_content = render_item(file);
            } else {
                templated_content = html`${templated_content}, ${render_item(file)}`;
            }
        }

        return templated_content;
    }

    function getHTMLStringFromTemplate(template) {
        const element = document.createElement("div");
        template({}, element);

        return $sce.trustAsHtml(element.innerHTML);
    }
            <span ng-bind-html="card_ctrl.getCardFieldPermissionsValue(card_field.granted_groups, card_ctrl.filter_terms)"></span>
/**
 * 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/>.
 */

import { highlightFilterElements } from "./highlight-filter-template";
import type { UpdateFunctionWithMethods } from "hybrids";

jest.mock("escape-string-regexp", () => ({
    default: jest.requireActual("escape-string-regexp"),
}));

const TEST_PHRASE = "Prefix Highlight Suffix";

describe("highlight-filter-template", () => {
    it("should highlight a matching phrase", () => {
        expect(getElementFromTemplate(highlightFilterElements(TEST_PHRASE, "highlight")))
            .toMatchInlineSnapshot(`
            <div>
              
              
              Prefix 
              
              <span
                class="highlight"
              >
                Highlight
              </span>
              
               Suffix
            </div>
        `);
    });

    it("should highlight nothing if no match found", () => {
        expect(getElementFromTemplate(highlightFilterElements(TEST_PHRASE, "no match")))
            .toMatchInlineSnapshot(`
            <div>
              
              
              Prefix Highlight Suffix
            </div>
        `);
    });

    it("should highlight nothing for the undefined filter", () => {
        expect(getElementFromTemplate(highlightFilterElements(TEST_PHRASE, undefined)))
            .toMatchInlineSnapshot(`
            <div>
              Prefix Highlight Suffix
            </div>
        `);
    });

    it("should work correctly for number filters", () => {
        expect(getElementFromTemplate(highlightFilterElements("3210123", 0)))
            .toMatchInlineSnapshot(`
            <div>
              
              
              321
              
              <span
                class="highlight"
              >
                0
              </span>
              
              123
            </div>
        `);
    });

    it("should work when the text context is undefined", () => {
        expect(getElementFromTemplate(highlightFilterElements(undefined, "nothing")))
            .toMatchInlineSnapshot(`
            <div>
              
            </div>
        `);
    });

    it("should work correctly for number text", () => {
        expect(getElementFromTemplate(highlightFilterElements(3210123, "0")))
            .toMatchInlineSnapshot(`
            <div>
              
              
              321
              
              <span
                class="highlight"
              >
                0
              </span>
              
              123
            </div>
        `);
    });

    it("should highlight nothing if empty filter string passed", () => {
        expect(getElementFromTemplate(highlightFilterElements(TEST_PHRASE, "")))
            .toMatchInlineSnapshot(`
            <div>
              Prefix Highlight Suffix
            </div>
        `);
    });

    it("should highlight more that one element", () => {
        expect(getElementFromTemplate(highlightFilterElements(TEST_PHRASE, "gh")))
            .toMatchInlineSnapshot(`
            <div>
              
              
              Prefix Hi
              
              <span
                class="highlight"
              >
                gh
              </span>
              
              li
              
              <span
                class="highlight"
              >
                gh
              </span>
              
              t Suffix
            </div>
        `);
    });

    it("highlights each matching search terms", () => {
        expect(getElementFromTemplate(highlightFilterElements(TEST_PHRASE, "suffix highlight")))
            .toMatchInlineSnapshot(`
            <div>
              
              
              Prefix 
              
              <span
                class="highlight"
              >
                Highlight
              </span>
              
               
              
              <span
                class="highlight"
              >
                Suffix
              </span>
            </div>
        `);
    });

    it("should escape regexp search terms", () => {
        expect(
            getElementFromTemplate(
                highlightFilterElements("Prefix (Highlight) Suffix", "(Highlight)")
            )
        ).toMatchInlineSnapshot(`
            <div>
              
              
              Prefix 
              
              <span
                class="highlight"
              >
                (Highlight)
              </span>
              
               Suffix
            </div>
        `);
    });
});

function getElementFromTemplate(template: UpdateFunctionWithMethods<unknown>): HTMLElement {
    const element = document.createElement("div");
    template(document.body, element);

    return element;
}
/**
 * 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/>.
 */

import type { UpdateFunctionWithMethods } from "hybrids";
import { html } from "hybrids";
import { Classifier } from "./highlight/Classifier";
import { HighlightedText } from "./highlight/HighlightedText";
export function highlightFilterElements(
    content: string | number | undefined,
    search: string | number | undefined
): UpdateFunctionWithMethods<unknown> {
    const text_content = content?.toString();
    if (
        text_content === "" ||
        text_content === undefined ||
        search === "" ||
        search === undefined
    ) {
        return html`${text_content}`;
    }

    const classifier = Classifier(search.toString());
    const templates = classifier.classify(text_content).map((highlighted_text) => {
        if (!HighlightedText.isHighlight(highlighted_text)) {
            return html`${highlighted_text.content}`;
        }
        return html`<span class="highlight">${highlighted_text.content}</span>`;
    });
    return html`${templates}`;
}
import { highlightFilterElements } from "./highlight-filter-template";
    function getHTMLStringFromTemplate(template) {
        const element = document.createElement("div");
        template({}, element);

        return element.innerHTML;
        if (text === null) {
            return null;
        return getHTMLStringFromTemplate(highlightFilterElements(text, search));
    .module("card-fields", [])
            external: ["angular", "hybrids"],
                    hybrids: "hybrids",