CVE-2023-28629 (published 2023-03-27T21:15Z)

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.

CVSS v3 base score: 5.4, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-79
cwe_name: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
description: 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.
extended_description: 
            Cross-site scripting (XSS) vulnerabilities occur when:
            
               Untrusted data enters a web application, typically from a web request.
               The web application dynamically generates a web page that contains this untrusted data.
               During page generation, the application does not prevent the data from containing content that is executable by a web browser, such as JavaScript, HTML tags, HTML attributes, mouse events, Flash, ActiveX, etc.
               A victim visits the generated web page through a web browser, which contains malicious script that was injected using the untrusted data.
               Since the script comes from a web page that was sent by the web server, the victim's web browser executes the malicious script in the context of the web server's domain.
               This effectively violates the intention of the web browser's same-origin policy, which states that scripts in one domain should not be able to access resources or run code in a different domain.
            
            There are three main kinds of XSS:
            
               
                  Type 1: Reflected XSS (or Non-Persistent) - 
         			The server reads data directly from the HTTP request and reflects it back in the HTTP response. Reflected XSS exploits occur when an attacker causes a victim to supply dangerous content to a vulnerable web application, which is then reflected back to the victim and executed by the web browser. The most common mechanism for delivering malicious content is to include it as a parameter in a URL that is posted publicly or e-mailed directly to the victim. URLs constructed in this manner constitute the core of many phishing schemes, whereby an attacker convinces a victim to visit a URL that refers to a vulnerable site. After the site reflects the attacker's content back to the victim, the content is executed by the victim's browser.
               
                  Type 2: Stored XSS (or Persistent) - 
               The application stores dangerous data in a database, message forum, visitor log, or other trusted data store. At a later time, the dangerous data is subsequently read back into the application and included in dynamic content. From an attacker's perspective, the optimal place to inject malicious content is in an area that is displayed to either many users or particularly interesting users. Interesting users typically have elevated privileges in the application or interact with sensitive data that is valuable to the attacker. If one of these users executes malicious content, the attacker may be able to perform privileged operations on behalf of the user or gain access to sensitive data belonging to the user. For example, the attacker might inject XSS into a log message, which might not be handled properly when an administrator views the logs.
            
               
                  Type 0: DOM-Based XSS - 
               In DOM-based XSS, the client performs the injection of XSS into the page; in the other types, the server performs the injection. DOM-based XSS generally involves server-controlled, trusted script that is sent to the client, such as Javascript that performs sanity checks on a form before the user submits it. If the server-supplied script processes user-supplied data and then injects it back into the web page (such as with dynamic HTML), then DOM-based XSS is possible.
            
            
            Once the malicious script is injected, the attacker can perform a variety of malicious activities. The attacker could transfer private information, such as cookies that may include session information, from the victim's machine to the attacker. The attacker could send malicious requests to a web site on behalf of the victim, which could be especially dangerous to the site if the victim has administrator privileges to manage that site. Phishing attacks could be used to emulate trusted web sites and trick the victim into entering a password, allowing the attacker to compromise the victim's account on that web site. Finally, the script could exploit a vulnerability in the web browser itself possibly taking over the victim's machine, sometimes referred to as "drive-by hacking."
            In many cases, the attack can be launched without the victim even being aware of it. Even with careful users, attackers frequently use a variety of methods to encode the malicious portion of the attack, such as URL encoding or Unicode, so the request looks less suspicious.
         
         

Fix information:
repo_url: https://github.com/gocd/gocd
commit_details: repo_url: https://github.com/gocd/gocd
author: Chad Wilson
author_date: 2022-12-04 00:08:05+08:00
msg: Improve escaping on legacy Freemarker templates
repository: repo_url: https://github.com/gocd/gocd
repo_name: gocd/gocd
description: GoCD - Continuous Delivery server main repository
repo_language: Java
owner: gocd
file_changes: filename: MaterialRevisionsJsonBuilder.java
change_type: ModificationType.MODIFY
diff: @@ -33,10 +33,10 @@ import static com.thoughtworks.go.util.DateUtils.formatISO8601;
 import static java.lang.String.valueOf;
 
 public class MaterialRevisionsJsonBuilder extends ModificationVisitorAdapter {
-    private List materials = new ArrayList();
+    private final List<Object> materials = new ArrayList<>();
     private Map<String, Object> materialJson;
-    private List modificationsJson;
-    private List modifiedFilesJson;
+    private List<Object> modificationsJson;
+    private List<Object> modifiedFilesJson;
     private boolean includeModifiedFiles = true;
     private final CommentRenderer commentRenderer;
     private MaterialRevision revision;
@@ -48,9 +48,9 @@ public class MaterialRevisionsJsonBuilder extends ModificationVisitorAdapter {
     @Override
     public void visit(MaterialRevision revision) {
         this.revision = revision;
-        modificationsJson = new ArrayList();
+        modificationsJson = new ArrayList<>();
 
-        materialJson = new LinkedHashMap();
+        materialJson = new LinkedHashMap<>();
         materialJson.put("revision", revision.getRevision().getRevision());
         materialJson.put("revision_href", revision.getRevision().getRevisionUrl());
         materialJson.put("user", revision.buildCausedBy());
@@ -68,7 +68,7 @@ public class MaterialRevisionsJsonBuilder extends ModificationVisitorAdapter {
 
     @Override
     public void visit(Modification modification) {
-        modifiedFilesJson = new ArrayList();
+        modifiedFilesJson = new ArrayList<>();
 
         Map<String, Object> jsonMap = new LinkedHashMap<>();
         jsonMap.put("user", modification.getUserDisplayName());
@@ -97,7 +97,7 @@ public class MaterialRevisionsJsonBuilder extends ModificationVisitorAdapter {
         modifiedFilesJson.add(jsonMap);
     }
 
-    public List json() {
+    public List<Object> json() {
         return materials;
     }
 

programming_language: Java
filename: freemarker-config.xml
change_type: ModificationType.MODIFY
diff: @@ -25,12 +25,17 @@
             </list>
         </property>
         <property name="defaultEncoding" value="utf-8"/>
+        <property name="freemarkerSettings">
+          <props>
+            <prop key="recognizeStandardFileExtensions">true</prop>
+          </props>
+        </property>
     </bean>
 
     <bean id="freeMarkerViewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
         <property name="cache" value="true"/>
         <property name="prefix" value=""/>
-        <property name="suffix" value=".ftl"/>
+        <property name="suffix" value=".ftlh"/>
         <property name="exposeSessionAttributes" value="true"/>
         <property name="exposeSpringMacroHelpers" value="true"/>
         <property name="requestContextAttribute" value="req"/>

programming_language: HTML
filename: manifest.js
change_type: ModificationType.MODIFY
diff: @@ -19,7 +19,7 @@
 //= link application.js
 //= link lib/d3-3.1.5.min.js
 
-// Used by legacy templates `_header.ftl`
+// Used by legacy templates `_header.ftlh`
 //= link application.css
 //= link patterns/application.css
 //= link css/application.css

programming_language: Go
filename: _artifacts.ftlh
change_type: ModificationType.RENAME
diff: @@ -23,6 +23,6 @@
         </#if>
     </#if>
   <div class="files">
-      <#include "../shared/_artifacts.ftl">
+      <#include "../shared/_artifacts.ftlh">
   </div>
 </div>

programming_language: HTML
filename: _build_detail_summary_jstemplate.ftlh
change_type: ModificationType.RENAME
diff: @@ -33,7 +33,7 @@
   </#if>
 </li>
   <li><span class="header">Completed on: </span><span id="build_completed_date">${r'${% build.build_completed_date %}'}</span></li>
-<li><span class="header">Build cause: </span><span id="stage-${r'${% build.id %}'}-buildCause">${presenter.buildCauseMessage?html?html}</span></li>
+<li><span class="header">Build cause: </span><span id="stage-${r'${% build.id %}'}-buildCause"><#noautoesc>${presenter.buildCauseMessage?html?html}</#noautoesc></span></li>
   {if build.current_status.toLowerCase() == 'passed' ||  build.current_status.toLowerCase() == 'failed'}
 <li><span class="header">Duration: </span><span>${r"${% moment.duration(parseInt(build.current_build_duration), 's').humanizeForGoCD() %}"}</span></li>
 {/if}

programming_language: HTML
filename: _buildoutput.ftlh
change_type: ModificationType.RENAME
diff: @@ -15,5 +15,5 @@
  -->
 <#-- @ftlvariable name="buildoutput_extra_attrs" type="java.lang.String" -->
 <div id="tab-content-of-console" class="ansi-color-toggle" ${buildoutput_extra_attrs}>
-    <#include "_build_output_raw.ftl">
+    <#include "_build_output_raw.ftlh">
 </div>

programming_language: HTML
filename: _material_revisions_jstemplate.ftl
change_type: ModificationType.MODIFY
diff: @@ -46,7 +46,7 @@
                     <span title="Comment" class="comment">
 
                         {if revision.scmType == 'Package' }
-                            <#include '../shared/_package_material_revision_comment.ftl'>
+                            <#include '../shared/_package_material_revision_comment.ftlh'>
                         {else}
                             <#noparse>"${%comment.replace(/\n/g,"<br>")%}"</#noparse>
                         {/if}

programming_language: HTML
filename: _materials.ftlh
change_type: ModificationType.RENAME
diff: @@ -16,7 +16,7 @@
 <#-- @ftlvariable name="modification_extra_attrs" type="java.lang.String" -->
 <div id="tab-content-of-materials" class="widget" ${modification_extra_attrs}>
     <script type="text/javascript">
-        var json = ${presenter.getMaterialRevisionsJson()};
+        var json = ${presenter.getMaterialRevisionsJson()?no_esc};
     </script>
     <script type="text/javascript">
         Event.observe(window, 'load', function(){

programming_language: HTML
filename: _test_output_config.ftlh
change_type: ModificationType.RENAME
diff: @@ -17,17 +17,19 @@
 <p>No test output found. Make sure your job configuration includes a test artifact.</p><br/>
 <p>For example:</p><br/>
 <pre>
+<#noautoesc>
 &lt;job name="example"&gt;
-  ${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;
+${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;
 &lt;/job&gt;
+</#noautoesc>
 </pre>
 <br/>
 <p><a href="${currentGoCDVersion.docsUrl('/configuration/managing_artifacts_and_reports.html')}" target='_blank' class="obvious_link">Help Topic: Managing Artifacts and Reports</a></p>

programming_language: HTML
filename: _tests.ftlh
change_type: ModificationType.RENAME
diff: @@ -19,7 +19,7 @@
         <#if presenter.hasTests()>
             <iframe sandbox="allow-scripts" src="${req.getContextPath()}/${presenter.indexPageURL}" width="95%" height="500" frameborder="0"></iframe>
         <#else>
-            <#include "_test_output_config.ftl">
+            <#include "_test_output_config.ftlh">
         </#if>
     </div>
 </div>

programming_language: HTML
filename: build_detail_page.ftlh
change_type: ModificationType.RENAME
diff: @@ -16,9 +16,9 @@
 <#assign title = "${presenter.buildName} Job Details - Go">
 <#assign _page_title = "Job Details for ${presenter.buildLocatorForDisplay}">
 <#assign current_tab = "build">
-<#include "../shared/_header.ftl">
+<#include "../shared/_header.ftlh">
 
-<#include "_build_detail_summary_jstemplate.ftl">
+<#include "_build_detail_summary_jstemplate.ftlh">
 <div id="yui-main">
   <div class="yui-b">
     <!-- breadcrumbs -->
@@ -26,7 +26,7 @@
       <#assign pipelineName="${presenter.pipelineName}">
       <#assign stageLocator="${presenter.stageLocator}">
 
-      <#include "../shared/_job_details_breadcrumbs.ftl">
+      <#include "../shared/_job_details_breadcrumbs.ftlh">
     <!-- /breadcrumbs -->
 
     <div class="content_wrapper_outer">
@@ -34,7 +34,7 @@
         <div class="content_wrapper_inner">
           <div id="build-status-panel" class="bd-container rounded-corner-for-pipeline">
             <div class="maincol build_detail">
-                <#include "../shared/_flash_message.ftl">
+                <#include "../shared/_flash_message.ftlh">
                 <#assign jobConfigName = "${presenter.buildName}">
               <div id="build_detail_summary_container" class="build_detail_summary">
                 <ul id="build-detail-summary" class="summary">
@@ -42,7 +42,7 @@
                   <li><span class="header">Agent: </span><span id="agent_name">Loading...</span></li>
                   <li><span class="header">Completed on: </span><span id="build_completed_date">Loading...</span></li>
                   <li><span class="header">Build cause: </span><span
-                    id="stage-${presenter.id?c}-buildCause">${presenter.buildCauseMessage?html}</span></li>
+                    id="stage-${presenter.id?c}-buildCause">${presenter.buildCauseMessage}</span></li>
                   <li class="timer_area">
                     <div class="progress-info">
                       <div id="${presenter.buildName}_progress_bar" class="progress-bar" style="display: none;">
@@ -93,7 +93,7 @@
                 </div>
 
               <div class="sidebar_history">
-                  <#include "../sidebar/_sidebar_build_list.ftl">
+                  <#include "../sidebar/_sidebar_build_list.ftlh">
               </div>
                 <div class="build_detail_container sub_tab_container rounded-corner-for-tab-container">
 
@@ -102,20 +102,20 @@
                     <div class="clear"></div>
 
                       <#assign buildoutput_extra_attrs="">
-                      <#include "_buildoutput.ftl">
+                      <#include "_buildoutput.ftlh">
 
                       <#assign tests_extra_attrs="style='display:none'">
-                      <#include "_tests.ftl">
+                      <#include "_tests.ftlh">
 
                       <#assign artifacts_extra_attrs="style='display:none'">
-                      <#include "_artifacts.ftl">
+                      <#include "_artifacts.ftlh">
 
                       <#assign modification_extra_attrs="style='display:none'">
-                      <#include "_materials.ftl">
+                      <#include "_materials.ftlh">
                       <#list presenter.customizedTabs as tab>
                         <#assign customized_name="${tab.name}">
                         <#assign customized_path="${tab.path}">
-                        <#include "_customized.ftl">
+                        <#include "_customized.ftlh">
                       </#list>
                   </div>
                 </div>
@@ -128,4 +128,4 @@
   </div>
 </div>
 </div>
-<#include "../shared/_footer.ftl">
+<#include "../shared/_footer.ftlh">

programming_language: HTML
filename: exceptions_page.ftlh
change_type: ModificationType.RENAME
diff: @@ -15,15 +15,14 @@
  -->
 <#-- @ftlvariable name="errorMessage" type="java.lang.String" -->
 <#assign title = 'Exception Detail - GoCD'>
-<#include "shared/_header.ftl">
-<#include "shared/_flash_message.ftl">
+<#include "shared/_header.ftlh">
+<#include "shared/_flash_message.ftlh">
     <div id="yui-main">
         <div class="yui-b"></div>
     </div>
     <script type="text/javascript">
-        $('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});
+        $('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});
     </script>
 
 </body>

programming_language: HTML
filename: html.ftlh
change_type: ModificationType.RENAME
diff: @@ -16,7 +16,7 @@
  -->
 <#-- @ftlvariable name="jobIdentifier" type="com.thoughtworks.go.domain.JobIdentifier" -->
 <#assign title = "Artifacts for ${jobIdentifier.pipelineName} > ${jobIdentifier.pipelineLabel} > ${jobIdentifier.stageName} > ${jobIdentifier.stageCounter} > ${jobIdentifier.buildName}">
-<#include "../shared/_header.ftl">
+<#include "../shared/_header.ftlh">
 
 <div id="yui-main">
     <div class="yui-b">
@@ -26,10 +26,10 @@
                 <h2>${title}</h2>
 
                 <div id="artifacts" class="container-in-body">
-                    <#include "../shared/_artifacts.ftl">
+                    <#include "../shared/_artifacts.ftlh">
                 </div>
             </div>
         </div>
 
-<#include "../shared/_footer.ftl">
+<#include "../shared/_footer.ftlh">
 

programming_language: HTML
filename: _artifact_entry.ftlh
change_type: ModificationType.RENAME
diff: @@ -13,4 +13,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  -->
-${presenter.renderArtifactFiles(req.getContextPath())}
+${presenter.renderArtifactFiles(req.getContextPath())?no_esc}

programming_language: TypeScript
filename: _artifacts.ftlh
change_type: ModificationType.RENAME
diff: @@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  -->
-<#include '_artifact_entry.ftl'>
+<#include '_artifact_entry.ftlh'>
 
 <script type="text/javascript">
     try{

programming_language: TypeScript
filename: _sidebar_build_list.ftlh
change_type: ModificationType.RENAME
diff: @@ -37,7 +37,7 @@
     </div>
     <script type="text/javascript">
         <#list presenter.recent25 as listPresenter>
-        json_to_css.update_build_list(eval(${listPresenter.toJsonString()}), ${listPresenter?counter}, "${req.getContextPath()}/${concatenatedStageBarCancelledIconFilePath}");
+        json_to_css.update_build_list(eval(${listPresenter.toJsonString()?no_esc}), ${listPresenter?counter}, "${req.getContextPath()}/${concatenatedStageBarCancelledIconFilePath}");
         </#list>
 
       jQuery(function() {

programming_language: HTML
filename: AbstractFreemarkerTemplateTest.java
change_type: ModificationType.RENAME
diff: @@ -14,19 +14,24 @@
  * limitations under the License.
  */
 
-package com.thoughtworks.go.server.view.velocity;
+package com.thoughtworks.go.server.view.freemarker;
 
 import com.thoughtworks.go.server.service.*;
+import org.jsoup.parser.Parser;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.AdditionalAnswers;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.*;
 
 @ExtendWith(MockitoExtension.class)
 public class AbstractFreemarkerTemplateTest {
     protected TestFreeMarkerView view;
+    protected final Parser parser = Parser.htmlParser().setTrackErrors(100);
+
     @Mock
     private RailsAssetsService railsAssetsService;
     @Mock
@@ -51,4 +56,10 @@ public class AbstractFreemarkerTemplateTest {
         lenient().doReturn(securityService).when(view).getSecurityService();
         lenient().doReturn(maintenanceModeService).when(view).getMaintenanceModeService();
     }
+
+    @AfterEach
+    public void checkParseErrors() {
+        assertThat(parser.isTrackErrors()).isTrue();
+        assertThat(parser.getErrors()).isEmpty();
+    }
 }

programming_language: Java
filename: BuildDetailPageFreeMarkerTemplateTest.java
change_type: ModificationType.RENAME
diff: @@ -13,13 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.thoughtworks.go.server.view.velocity;
+package com.thoughtworks.go.server.view.freemarker;
 
 import com.thoughtworks.go.config.Tabs;
 import com.thoughtworks.go.config.TrackingTool;
 import com.thoughtworks.go.config.materials.MaterialConfigs;
 import com.thoughtworks.go.config.materials.git.GitMaterial;
 import com.thoughtworks.go.config.materials.git.GitMaterialConfig;
+import com.thoughtworks.go.domain.JobInstance;
 import com.thoughtworks.go.domain.JobInstances;
 import com.thoughtworks.go.domain.MaterialRevisions;
 import com.thoughtworks.go.domain.Pipeline;
@@ -27,7 +28,7 @@ import com.thoughtworks.go.domain.materials.Modification;
 import com.thoughtworks.go.helper.StageMother;
 import com.thoughtworks.go.server.presentation.models.JobDetailPresentationModel;
 import com.thoughtworks.go.server.service.ArtifactsService;
-import org.jsoup.Jsoup;
+import org.apache.commons.text.StringEscapeUtils;
 import org.jsoup.nodes.Document;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -41,27 +42,56 @@ import static com.thoughtworks.go.helper.JobInstanceMother.building;
 import static com.thoughtworks.go.helper.MaterialConfigsMother.gitMaterialConfig;
 import static com.thoughtworks.go.helper.PipelineConfigMother.pipelineConfig;
 import static com.thoughtworks.go.helper.PipelineMother.schedule;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.*;
 
 public class BuildDetailPageFreeMarkerTemplateTest extends AbstractFreemarkerTemplateTest {
 
+    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)";
+
     @BeforeEach
     public void setUp() throws Exception {
-        super.setUp("build_detail/build_detail_page.ftl");
+        super.setUp("build_detail/build_detail_page.ftlh");
     }
 
     @Test
-    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 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;");
     }
 
     @Test
-    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;"));
+    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>");
     }
 
     @Test
@@ -70,9 +100,9 @@ public class BuildDetailPageFreeMarkerTemplateTest extends AbstractFreemarkerTem
         when(jobDetailPresentationModel.hasTests()).thenReturn(true);
         when(jobDetailPresentationModel.getCustomizedTabs()).thenReturn(new Tabs());
 
-        Document actualDoc = Jsoup.parse(view.render(minimalModelFrom(jobDetailPresentationModel)));
-
-        assertThat(actualDoc.select("#tab-content-of-tests").last().html(), containsString("<iframe sandbox=\"allow-scripts\""));
+        Document actualDoc = parser.parseInput(view.render(minimalModelFrom(jobDetailPresentationModel)), "");
+        assertThat(actualDoc.select("#tab-content-of-tests").last().html())
+            .contains("<iframe sandbox=\"allow-scripts\"");
     }
 
     private Map<String, Object> createJobDetailModel() {
@@ -80,12 +110,17 @@ public class BuildDetailPageFreeMarkerTemplateTest extends AbstractFreemarkerTem
 
         MaterialRevisions materialRevisions = new MaterialRevisions();
         materialRevisions.addRevision(new GitMaterial(gitMaterialConfig),
-                new Modification("Ernest Hemingway <oldman@sea.com>", "comment", "email", new Date(), "12", ""));
+            new Modification("Ernest Hemingway <oldman@sea.com>", "comment", "email", new Date(), "12", ""));
 
         Pipeline pipeline = schedule(pipelineConfig("pipeline", new MaterialConfigs(gitMaterialConfig)), createWithModifications(materialRevisions, ""));
-        JobDetailPresentationModel model = new JobDetailPresentationModel(building("job"), new JobInstances(), null,
-                pipeline, new Tabs(), new TrackingTool(),
-                mock(ArtifactsService.class), StageMother.custom("stage"));
+        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"));
 
         return minimalModelFrom(model);
     }

programming_language: Java
filename: ExceptionTemplateTest.java
change_type: ModificationType.RENAME
diff: @@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.thoughtworks.go.server.view.velocity;
+package com.thoughtworks.go.server.view.freemarker;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -21,11 +21,10 @@ import org.junit.jupiter.api.Test;
 import java.util.HashMap;
 import java.util.Map;
 
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsString;
+import static org.assertj.core.api.Assertions.assertThat;
 
 public class ExceptionTemplateTest extends AbstractFreemarkerTemplateTest {
-    public static final String TEMPLATE_PATH = "exceptions_page.ftl";
+    public static final String TEMPLATE_PATH = "exceptions_page.ftlh";
 
     @BeforeEach
     public void setUp() throws Exception {
@@ -38,12 +37,21 @@ public class ExceptionTemplateTest extends AbstractFreemarkerTemplateTest {
         data.put("errorMessage", "ERROR MESSAGE");
 
         String output = view.render(data);
-        assertThat(output, containsString("$('trans_content').update(\"Sorry, an unexpected error occurred [ERROR MESSAGE]. :( Please check the server logs for more information.\");"));
+        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.\");");
     }
 
     @Test
     public void shouldHaveTheGenericMessageInOutputOfTemplateWhenCustomErrorMessageIsNotProvided() {
         String output = view.render(new HashMap<>());
-        assertThat(output, containsString("$('trans_content').update(\"Sorry, an unexpected error occurred. :( 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.\");");
     }
 }

programming_language: Java
filename: TestFreeMarkerView.java
change_type: ModificationType.RENAME
diff: @@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.thoughtworks.go.server.view.velocity;
+package com.thoughtworks.go.server.view.freemarker;
 
 import com.thoughtworks.go.server.newsecurity.SessionUtilsHelper;
 import com.thoughtworks.go.server.web.GoCDFreeMarkerView;
@@ -24,6 +24,7 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
 
 import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.Properties;
 
 class TestFreeMarkerView extends GoCDFreeMarkerView {
 
@@ -34,6 +35,10 @@ class TestFreeMarkerView extends GoCDFreeMarkerView {
         configurer.setDefaultEncoding(StandardCharsets.UTF_8.name());
         configurer.setTemplateLoaderPath("file:src/main/webapp/WEB-INF/vm");
 
+        Properties settings = new Properties();
+        settings.put("recognizeStandardFileExtensions", "true");
+        configurer.setFreemarkerSettings(settings);
+
         setConfiguration(configurer.createConfiguration());
         setServletContext(new MockServletContext());
         setRequestContextAttribute(REQUEST_CONTEXT_ATTRIBUTE);

programming_language: Java
repo_url: https://github.com/gocd/gocd
commit_details: repo_url: https://github.com/gocd/gocd
author: Chad Wilson
author_date: 2022-12-03 18:11:45+08:00
msg: Encode VSM node attributes for HTML
repository: repo_url: https://github.com/gocd/gocd
repo_name: gocd/gocd
description: GoCD - Continuous Delivery server main repository
repo_language: Java
owner: gocd
file_changes: filename: vsm_renderer.js
change_type: ModificationType.MODIFY
diff: @@ -375,7 +375,7 @@ Graph_Renderer = function (container) {
                 + '<div class="usercomment wraptext">' + parseComment(instance.comment) + '</div>'
                 + '<div class="author">'
                 + '<p>' + _.escape(instance.user) + ' </p>'
-                + '<p>' + instance.modified_time + '</p>'
+                + '<p>' + _.escape(instance.modified_time) + '</p>'
                 + '</div>'
                 + '</li>';
     }
@@ -387,10 +387,10 @@ Graph_Renderer = function (container) {
             var package_comment = comment_map['COMMENT'];
             var trackback_url = comment_map['TRACKBACK_URL'];
             if (typeof package_comment !== "undefined" || package_comment != null) {
-                comment_markup = package_comment + "<br/>";
+                comment_markup = _.escape(package_comment) + "<br/>";
             }
             if (typeof trackback_url !== "undefined" || trackback_url != null) {
-                return comment_markup + "Trackback: " + "<a href=" + trackback_url + ">" + trackback_url + "</a>";
+                return comment_markup + "Trackback: " + "<a href=" + _.escape(trackback_url) + ">" + _.escape(trackback_url) + "</a>";
             }
             return comment_markup + "Trackback: " + "Not Provided";
         }
@@ -404,10 +404,10 @@ Graph_Renderer = function (container) {
             var package_comment = comment_map['COMMENT'];
             var trackback_url = comment_map['TRACKBACK_URL'];
             if (typeof package_comment !== "undefined" || package_comment != null) {
-                comment_tooltip = package_comment + "\n";
+                comment_tooltip = _.escape(package_comment) + "\n";
             }
             if (typeof trackback_url !== "undefined" || trackback_url != null) {
-                return comment_tooltip + "Trackback: " + trackback_url;
+                return comment_tooltip + "Trackback: " + _.escape(trackback_url);
             }
             return comment_tooltip + "Trackback: " + "Not Provided";
         }
@@ -460,7 +460,7 @@ Graph_Renderer = function (container) {
         var gui = '';
         gui += '<h3 title="' + node.name + '" class="restricted">' + node.name + '</h3>';
         if (node.message) {
-            gui += '<div class="message restricted"><span>' + node.message + '</span></div>';
+            gui += '<div class="message restricted"><span>' + _.escape(node.message) + '</span></div>';
         }
         gui += '<div class="actions restricted">';
         gui += '<button class="pin" title="Keep dependencies highlighted" /></div>';
@@ -470,7 +470,7 @@ Graph_Renderer = function (container) {
     function renderWarning(node) {
         var gui = '';
         if (node.message) {
-            gui += '<div class="warning"><span>' + node.message + '</span></div>';
+            gui += '<div class="warning"><span>' + _.escape(node.message) + '</span></div>';
         }
         return gui;
     }
@@ -479,7 +479,7 @@ Graph_Renderer = function (container) {
         var gui = '';
         gui += '<h3 title="' + node.name + '" class="deleted">' + node.name + '</h3>';
         if (node.message) {
-            gui += '<div class="message deleted"><span>' + node.message + '</span></div>';
+            gui += '<div class="message deleted"><span>' + _.escape(node.message) + '</span></div>';
         }
         gui += '<div class="actions deleted"><button class="pin" title="Keep dependencies highlighted" /></div>';
         return gui;
@@ -491,10 +491,10 @@ Graph_Renderer = function (container) {
         gui += '<li class="instance">';
         if (instance.label != '') {
             if (isCurrent) {
-                gui += '<h4 title="' + instance.label + '"><span class="pipeline_run_label">Instance: ' + instance.label + '</span></h4>';
+                gui += '<h4 title="' + _.escape(instance.label) + '"><span class="pipeline_run_label">Instance: ' + _.escape(instance.label) + '</span></h4>';
             }
             else {
-                gui += '<h4 title="' + instance.label + '"><span class="pipeline_run_label">Instance: ' + instance.label + '</span><span class="vsm_link_wrapper"><a href="' + instance.locator + '">' + 'VSM' + '</a></span></h4>';
+                gui += '<h4 title="' + _.escape(instance.label) + '"><span class="pipeline_run_label">Instance: ' + _.escape(instance.label) + '</span><span class="vsm_link_wrapper"><a href="' + instance.locator + '">' + 'VSM' + '</a></span></h4>';
             }
         }
         if(instance.locator.trim() != "") {

programming_language: JavaScript