CVE-2023-28629 (Other, published 2023-03-27T21:15Z)
Description: value: GoCD is an open source continuous delivery server. GoCD versions before 23.1.0 are vulnerable to a stored XSS vulnerability, where pipeline configuration with a malicious pipeline label configuration can affect browser display of pipeline runs generated from that configuration. An attacker that has permissions to configure GoCD pipelines could include JavaScript elements within the label template, causing a XSS vulnerability to be triggered for any users viewing the Value Stream Map or Job Details for runs of the affected pipeline, potentially allowing them to perform arbitrary actions within the victim's browser context rather than their own. This issue has been fixed in GoCD 23.1.0. Users are advised to upgrade. There are no known workarounds for this vulnerability.
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/gocd/gocd
Files changed:
server/src/main/java/com/thoughtworks/go/server/presentation/models/MaterialRevisionsJsonBuilder.java
server/src/main/resources/freemarker-config.xml
server/src/main/webapp/WEB-INF/rails/app/assets/config/manifest.js
server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftlh
server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftlh
server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftlh
server/src/main/webapp/WEB-INF/vm/build_detail/_material_revisions_jstemplate.ftl
server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftlh
server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftlh
server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftlh
server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftlh
server/src/main/webapp/WEB-INF/vm/exceptions_page.ftlh
server/src/main/webapp/WEB-INF/vm/rest/html.ftlh
server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftlh
server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftlh
server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftlh
server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/AbstractFreemarkerTemplateTest.java
server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/BuildDetailPageFreeMarkerTemplateTest.java
server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/ExceptionTemplateTest.java
server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/TestFreeMarkerView.java
Commit message: Improve escaping on legacy Freemarker templates


Vulnerable code:
    private List materials = new ArrayList();
    private List modificationsJson;
    private List modifiedFilesJson;
        modificationsJson = new ArrayList();
        materialJson = new LinkedHashMap();
        modifiedFilesJson = new ArrayList();
    public List json() {
        <property name="suffix" value=".ftl"/>
// Used by legacy templates `_header.ftl`
      <#include "../shared/_artifacts.ftl">
<li><span class="header">Build cause: </span><span id="stage-${r'${% build.id %}'}-buildCause">${presenter.buildCauseMessage?html?html}</span></li>
    <#include "_build_output_raw.ftl">
                            <#include '../shared/_package_material_revision_comment.ftl'>
        var json = ${presenter.getMaterialRevisionsJson()};
  ${tab} &lt;resources&gt;
  ${tab}${tab} &lt;resource&gt;java&lt;/resource&gt;
  ${tab} &lt;/resources&gt;
  <strong class='code'>${tab} &lt;artifacts&gt;
  ${tab}${tab} &lt;test src="target/reports" /&gt;
  ${tab} &lt;/artifacts&gt;</strong>
  ${tab} &lt;tasks&gt;
  ${tab}${tab} &lt;ant target="unit_tests" /&gt;
  ${tab} &lt;/tasks&gt;
            <#include "_test_output_config.ftl">
<#include "../shared/_header.ftl">
<#include "_build_detail_summary_jstemplate.ftl">
      <#include "../shared/_job_details_breadcrumbs.ftl">
                <#include "../shared/_flash_message.ftl">
                    id="stage-${presenter.id?c}-buildCause">${presenter.buildCauseMessage?html}</span></li>
                  <#include "../sidebar/_sidebar_build_list.ftl">
                      <#include "_buildoutput.ftl">
                      <#include "_tests.ftl">
                      <#include "_artifacts.ftl">
                      <#include "_materials.ftl">
                        <#include "_customized.ftl">
<#include "../shared/_footer.ftl">
<#include "shared/_header.ftl">
<#include "shared/_flash_message.ftl">
        $('trans_content').update("Sorry, an unexpected error occurred<#if errorMessage??> [${errorMessage?js_string?html}]</#if>. :( Please check the server logs for more information.");
        var transMessage = new TransMessage('trans_message', document.body,
                {type: TransMessage.TYPE_ERROR, offsetTop: 200, autoHide: false});
<#include "../shared/_header.ftl">
                    <#include "../shared/_artifacts.ftl">
<#include "../shared/_footer.ftl">
${presenter.renderArtifactFiles(req.getContextPath())}
<#include '_artifact_entry.ftl'>
        json_to_css.update_build_list(eval(${listPresenter.toJsonString()}), ${listPresenter?counter}, "${req.getContextPath()}/${concatenatedStageBarCancelledIconFilePath}");
package com.thoughtworks.go.server.view.velocity;
package com.thoughtworks.go.server.view.velocity;
import org.jsoup.Jsoup;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
        super.setUp("build_detail/build_detail_page.ftl");
    public void shouldEscapeBuildCauseOnVelocityTemplate() {
        Document actualDoc = Jsoup.parse(view.render(createJobDetailModel()));
        assertThat(actualDoc.select("#build-detail-summary").last().html(), containsString("modified by Ernest Hemingway &lt;oldman@sea.com&gt;"));
    public void shouldEscapeBuildCauseInTrimPathTemplate() {
        Document actualDoc = Jsoup.parse(view.render(createJobDetailModel()));
        assertThat(actualDoc.select("#build-summary-template").last().html(), containsString("modified by Ernest Hemingway &amp;lt;oldman@sea.com&amp;gt;"));
        Document actualDoc = Jsoup.parse(view.render(minimalModelFrom(jobDetailPresentationModel)));

        assertThat(actualDoc.select("#tab-content-of-tests").last().html(), containsString("<iframe sandbox=\"allow-scripts\""));
                new Modification("Ernest Hemingway <oldman@sea.com>", "comment", "email", new Date(), "12", ""));
        JobDetailPresentationModel model = new JobDetailPresentationModel(building("job"), new JobInstances(), null,
                pipeline, new Tabs(), new TrackingTool(),
                mock(ArtifactsService.class), StageMother.custom("stage"));
package com.thoughtworks.go.server.view.velocity;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
    public static final String TEMPLATE_PATH = "exceptions_page.ftl";
        assertThat(output, containsString("$('trans_content').update(\"Sorry, an unexpected error occurred [ERROR MESSAGE]. :( Please check the server logs for more information.\");"));
        assertThat(output, containsString("$('trans_content').update(\"Sorry, an unexpected error occurred. :( Please check the server logs for more information.\");"));
package com.thoughtworks.go.server.view.velocity;

Fixed code:
    private final List<Object> materials = new ArrayList<>();
    private List<Object> modificationsJson;
    private List<Object> modifiedFilesJson;
        modificationsJson = new ArrayList<>();
        materialJson = new LinkedHashMap<>();
        modifiedFilesJson = new ArrayList<>();
    public List<Object> json() {
        <property name="freemarkerSettings">
          <props>
            <prop key="recognizeStandardFileExtensions">true</prop>
          </props>
        </property>
        <property name="suffix" value=".ftlh"/>
// Used by legacy templates `_header.ftlh`
      <#include "../shared/_artifacts.ftlh">
<li><span class="header">Build cause: </span><span id="stage-${r'${% build.id %}'}-buildCause"><#noautoesc>${presenter.buildCauseMessage?html?html}</#noautoesc></span></li>
    <#include "_build_output_raw.ftlh">
                            <#include '../shared/_package_material_revision_comment.ftlh'>
        var json = ${presenter.getMaterialRevisionsJson()?no_esc};
<#noautoesc>
${tab}&lt;resources&gt;
${tab}${tab}&lt;resource&gt;java&lt;/resource&gt;
${tab}&lt;/resources&gt;
<strong class='code'>${tab}&lt;artifacts&gt;
${tab}${tab}&lt;test src="target/reports" /&gt;
${tab}&lt;/artifacts&gt;</strong>
${tab}&lt;tasks&gt;
${tab}${tab}&lt;ant target="unit_tests" /&gt;
${tab}&lt;/tasks&gt;
</#noautoesc>
            <#include "_test_output_config.ftlh">
<#include "../shared/_header.ftlh">
<#include "_build_detail_summary_jstemplate.ftlh">
      <#include "../shared/_job_details_breadcrumbs.ftlh">
                <#include "../shared/_flash_message.ftlh">
                    id="stage-${presenter.id?c}-buildCause">${presenter.buildCauseMessage}</span></li>
                  <#include "../sidebar/_sidebar_build_list.ftlh">
                      <#include "_buildoutput.ftlh">
                      <#include "_tests.ftlh">
                      <#include "_artifacts.ftlh">
                      <#include "_materials.ftlh">
                        <#include "_customized.ftlh">
<#include "../shared/_footer.ftlh">
<#include "shared/_header.ftlh">
<#include "shared/_flash_message.ftlh">
        $('trans_content').update("Sorry, an unexpected error occurred<#if errorMessage??> [${errorMessage?js_string?no_esc}]</#if>. :( Please check the server logs for more information.");
        var transMessage = new TransMessage('trans_message', document.body, {type: TransMessage.TYPE_ERROR, offsetTop: 200, autoHide: false});
<#include "../shared/_header.ftlh">
                    <#include "../shared/_artifacts.ftlh">
<#include "../shared/_footer.ftlh">
${presenter.renderArtifactFiles(req.getContextPath())?no_esc}
<#include '_artifact_entry.ftlh'>
        json_to_css.update_build_list(eval(${listPresenter.toJsonString()?no_esc}), ${listPresenter?counter}, "${req.getContextPath()}/${concatenatedStageBarCancelledIconFilePath}");
package com.thoughtworks.go.server.view.freemarker;
import org.jsoup.parser.Parser;
import org.junit.jupiter.api.AfterEach;
import static org.assertj.core.api.Assertions.assertThat;
    protected final Parser parser = Parser.htmlParser().setTrackErrors(100);


    @AfterEach
    public void checkParseErrors() {
        assertThat(parser.isTrackErrors()).isTrue();
        assertThat(parser.getErrors()).isEmpty();
    }
package com.thoughtworks.go.server.view.freemarker;
import com.thoughtworks.go.domain.JobInstance;
import org.apache.commons.text.StringEscapeUtils;
import static org.assertj.core.api.Assertions.assertThat;
    private static final String DODGY_LABEL = "label-1-<dodgy>-hello('world')";
    private static final String DODGY_LABEL_UNICODE_ESCAPED = "label-1-\\u003cdodgy\\u003e-hello(\\u0027world\\u0027)";

        super.setUp("build_detail/build_detail_page.ftlh");
    public void shouldEscapeBuildCauseForHtml() {
        Document actualDoc = parser.parseInput(view.render(createJobDetailModel()), "");
        assertThat(actualDoc.select("#build-detail-summary").last().html())
            .contains("modified by Ernest Hemingway &lt;oldman@sea.com&gt;");
    public void shouldEscapeBuildCauseForJavaScriptViaTrimPathTemplate() {
        Document actualDoc = parser.parseInput(view.render(createJobDetailModel()), "");
        assertThat(actualDoc.select("#build-summary-template").last().html())
            .contains("modified by Ernest Hemingway &amp;lt;oldman@sea.com&amp;gt;");
    }

    @Test
    public void shouldEscapeRenderedPipelineLabels() {
        Document actualDoc = parser.parseInput(view.render(createJobDetailModel()), "");

        // Header is escaped
        assertThat(actualDoc.select("#job_details_header").last().html())
            .contains(StringEscapeUtils.escapeHtml4(DODGY_LABEL));

        // Sidebar links are escaped
        assertThat(actualDoc.select("#buildlist-container ul li[id=build_list_1]").last().html())
            .contains(String.format("pipeline/%s/stage/1/job", StringEscapeUtils.escapeHtml4(DODGY_LABEL)));

        // Strange JS that updates sidebar should be correctly JS escaped
        assertThat(actualDoc.select("#build_history_holder script").last().html())
            .contains("json_to_css.update_build_list(eval({\"building_info\"")
            .contains(String.format("\"buildLocatorForDisplay\":\"pipeline/%s/stage/1/job\"", DODGY_LABEL_UNICODE_ESCAPED));
    }

    @Test
    public void shouldRenderArtifactFilesAsRawHtml() {
        Document actualDoc = parser.parseInput(view.render(createJobDetailModel()), "");
        assertThat(actualDoc.select("#tab-content-of-artifacts .files").last().html())
            .contains("<a href=\"null\"> console.log </a>");
        Document actualDoc = parser.parseInput(view.render(minimalModelFrom(jobDetailPresentationModel)), "");
        assertThat(actualDoc.select("#tab-content-of-tests").last().html())
            .contains("<iframe sandbox=\"allow-scripts\"");
            new Modification("Ernest Hemingway <oldman@sea.com>", "comment", "email", new Date(), "12", ""));
        JobInstance jobInstance = building("job");
        jobInstance.getIdentifier().setPipelineLabel(DODGY_LABEL);

        JobInstances recent25 = new JobInstances();
        recent25.add(jobInstance);
        JobDetailPresentationModel model = new JobDetailPresentationModel(jobInstance, recent25, null,
            pipeline, new Tabs(), new TrackingTool(),
            mock(ArtifactsService.class), StageMother.custom("stage"));
package com.thoughtworks.go.server.view.freemarker;
import static org.assertj.core.api.Assertions.assertThat;
    public static final String TEMPLATE_PATH = "exceptions_page.ftlh";
        assertThat(output).contains("$('trans_content').update(\"Sorry, an unexpected error occurred [ERROR MESSAGE]. :( Please check the server logs for more information.\");");
    }

    @Test
    public void shouldHaveErrorMessageAlongWithCustomMessageEscapedCorrectly() {
        Map<String, Object> data = new HashMap<>();
        data.put("errorMessage", "<Error> you shouldn't \"expect\"");

        String output = view.render(data);
        assertThat(output).contains("$('trans_content').update(\"Sorry, an unexpected error occurred [<Error> you shouldn\\'t \\\"expect\\\"]. :( Please check the server logs for more information.\");");
        assertThat(output).contains("$('trans_content').update(\"Sorry, an unexpected error occurred. :( Please check the server logs for more information.\");");
package com.thoughtworks.go.server.view.freemarker;
import java.util.Properties;
        Properties settings = new Properties();
        settings.put("recognizeStandardFileExtensions", "true");
        configurer.setFreemarkerSettings(settings);