CVE-2023-28629: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
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.
CWE 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.
CVSS v3 base score: 5.4
Published: 2023-03-27T21:15Z
Repository: https://github.com/gocd/gocd
Language: Other
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 date: 2022-12-04 17:38:43 +0800
Commit message: Improve escaping on legacy Freemarker templates

Diff: diff --git a/server/src/main/java/com/thoughtworks/go/server/presentation/models/MaterialRevisionsJsonBuilder.java b/server/src/main/java/com/thoughtworks/go/server/presentation/models/MaterialRevisionsJsonBuilder.java
index 9b6d072f7eb..471e2da1284 100644
--- a/server/src/main/java/com/thoughtworks/go/server/presentation/models/MaterialRevisionsJsonBuilder.java
+++ b/server/src/main/java/com/thoughtworks/go/server/presentation/models/MaterialRevisionsJsonBuilder.java
@@ -31,14 +31,14 @@ import java.util.Map;
 import static com.thoughtworks.go.config.materials.PackageMaterial.TYPE;
 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;
 
     public MaterialRevisionsJsonBuilder(CommentRenderer commentRenderer) {
@@ -46,13 +46,13 @@ 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());
         materialJson.put("date", formatISO8601(revision.getDateOfLatestModification()));
         materialJson.put("changed", valueOf(revision.isChanged()));
@@ -66,11 +66,11 @@ public class MaterialRevisionsJsonBuilder extends ModificationVisitorAdapter {
         material.toJson(materialJson, revision);
     }
 
     @Override
     public void visit(Modification modification) {
-        modifiedFilesJson = new ArrayList();
+        modifiedFilesJson = new ArrayList<>();
 
         Map<String, Object> jsonMap = new LinkedHashMap<>();
         jsonMap.put("user", modification.getUserDisplayName());
         jsonMap.put("revision", modification.getRevision());
         jsonMap.put("date", formatISO8601(modification.getModifiedTime()));
@@ -95,11 +95,11 @@ public class MaterialRevisionsJsonBuilder extends ModificationVisitorAdapter {
         jsonMap.put("fileName", file.getFileName());
 
         modifiedFilesJson.add(jsonMap);
     }
 
-    public List json() {
+    public List<Object> json() {
         return materials;
     }
 
     public void setIncludeModifiedFiles(boolean includeModifiedFiles) {
         this.includeModifiedFiles = includeModifiedFiles;
diff --git a/server/src/main/resources/freemarker-config.xml b/server/src/main/resources/freemarker-config.xml
index bad03f5fbcc..def23dbb230 100644
--- a/server/src/main/resources/freemarker-config.xml
+++ b/server/src/main/resources/freemarker-config.xml
@@ -23,16 +23,21 @@
             <list>
                 <value>/WEB-INF/vm/</value>
             </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"/>
         <property name="order" value="2"/>
         <property name="viewClass" value="com.thoughtworks.go.server.web.GoCDFreeMarkerView"/>
diff --git a/server/src/main/webapp/WEB-INF/rails/app/assets/config/manifest.js b/server/src/main/webapp/WEB-INF/rails/app/assets/config/manifest.js
index 9876eb5b9ed..edd9d35df75 100644
--- a/server/src/main/webapp/WEB-INF/rails/app/assets/config/manifest.js
+++ b/server/src/main/webapp/WEB-INF/rails/app/assets/config/manifest.js
@@ -17,11 +17,11 @@
 //= link_tree ../images
 
 //= 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
 //= link vm/application.css
 
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftlh
similarity index 96%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftlh
index 01d12e04476..5c82655dc73 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_artifacts.ftlh
@@ -21,8 +21,8 @@
             <a class="collapse-all" onclick="BuildDetail.expandAll()">(+) Expand All</a> <a class="expand-all" onclick="BuildDetail.collapseAll()">(-) Collapse All</a>
           </div>
         </#if>
     </#if>
   <div class="files">
-      <#include "../shared/_artifacts.ftl">
+      <#include "../shared/_artifacts.ftlh">
   </div>
 </div>
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftlh
similarity index 96%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftlh
index 5a437b2b6e8..c5cc306d103 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_build_detail_summary_jstemplate.ftlh
@@ -31,11 +31,11 @@
     <a href="${req.getContextPath()}/admin/status_reports/${elasticAgentPluginId}/agent/${elasticAgentId!'unassigned'}?job_id=${r'${% build.id %}'}"
        class="btn-primary btn-small status-report-btn-small">Check Agent Status</a>
   </#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}
 {if isEstimatable(build.current_status) }
 <li class="progress-info">
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_build_output_raw.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_build_output_raw.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_build_output_raw.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_build_output_raw.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftlh
similarity index 95%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftlh
index 4269b2fd99c..22b90103d02 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_buildoutput.ftlh
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  -->
 <#-- @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>
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_customized.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_customized.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_customized.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_customized.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_material_revisions_jstemplate.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_material_revisions_jstemplate.ftl
index 2d20961588c..4d4dc6cb11b 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_material_revisions_jstemplate.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_material_revisions_jstemplate.ftl
@@ -44,11 +44,11 @@
                     <br/>
 
                     <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}
                     </span>
                 </th>
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftlh
similarity index 94%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftlh
index 37eee535f05..0335bd32806 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_materials.ftlh
@@ -14,11 +14,11 @@
  * limitations under the License.
  -->
 <#-- @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(){
             $('tab-content-of-materials').innerHTML = $('tab-content-of-materials-template').value.process({revisions:json});
         });
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftlh
similarity index 74%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftlh
index 80e08bd003f..ba105c39f32 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_test_output_config.ftlh
@@ -15,19 +15,21 @@
  -->
 <#assign tab='&nbsp;&nbsp;&nbsp;'>
 <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>
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftlh
similarity index 95%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftlh
index 8e79e9d63b5..e466e5e7e43 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/_tests.ftlh
@@ -17,9 +17,9 @@
 <div id="tab-content-of-tests" class="widget" ${tests_extra_attrs}>
     <div class="files">
         <#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>
diff --git a/server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftl b/server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftlh
similarity index 90%
rename from server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftl
rename to server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftlh
index b784e1a388c..f68c3494ff2 100644
--- a/server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/build_detail/build_detail_page.ftlh
@@ -14,37 +14,37 @@
  * limitations under the License.
  -->
 <#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 -->
       <#assign current_page="build_detail">
       <#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">
       <div class="row">
         <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">
                   <li><span class="header">Scheduled on: </span><span id="build_scheduled_date">Loading...</span></li>
                   <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;">
                         <div id="${presenter.buildName}_progress" class="progress"></div>
                       </div>
@@ -91,33 +91,33 @@
 
                   <div class="clear"></div>
                 </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">
 
 
                   <div class="sub_tab_container_content">
                     <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>
               </div>
             </div>
@@ -126,6 +126,6 @@
       </div>
     </div>
   </div>
 </div>
 </div>
-<#include "../shared/_footer.ftl">
+<#include "../shared/_footer.ftlh">
diff --git a/server/src/main/webapp/WEB-INF/vm/exceptions_page.ftl b/server/src/main/webapp/WEB-INF/vm/exceptions_page.ftlh
similarity index 77%
rename from server/src/main/webapp/WEB-INF/vm/exceptions_page.ftl
rename to server/src/main/webapp/WEB-INF/vm/exceptions_page.ftlh
index 9a4ea036797..5bbcc285a67 100644
--- a/server/src/main/webapp/WEB-INF/vm/exceptions_page.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/exceptions_page.ftlh
@@ -13,18 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  -->
 <#-- @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>
 </html>
diff --git a/server/src/main/webapp/WEB-INF/vm/rest/html.ftl b/server/src/main/webapp/WEB-INF/vm/rest/html.ftlh
similarity index 90%
rename from server/src/main/webapp/WEB-INF/vm/rest/html.ftl
rename to server/src/main/webapp/WEB-INF/vm/rest/html.ftlh
index 0bc8c7ee238..4c8ed78b3e2 100644
--- a/server/src/main/webapp/WEB-INF/vm/rest/html.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/rest/html.ftlh
@@ -14,22 +14,22 @@
  * limitations under the License.
  *
  -->
 <#-- @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">
         <div class="bd-container rounded-corner-for-pipeline">
             <b class="c1"></b><b class="c2"></b><b class="c3"></b><b class="c4"></b>
             <div id="rest-output">
                 <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">
 
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftlh
similarity index 90%
rename from server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftlh
index ea7b98a8e86..3d7d4ace464 100644
--- a/server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/shared/_artifact_entry.ftlh
@@ -11,6 +11,6 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  -->
-${presenter.renderArtifactFiles(req.getContextPath())}
+${presenter.renderArtifactFiles(req.getContextPath())?no_esc}
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftlh
similarity index 94%
rename from server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftlh
index 04208c97e3c..dc9aea8ef15 100644
--- a/server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/shared/_artifacts.ftlh
@@ -11,11 +11,11 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * 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{
         BuildDetail.collapseAll();
     } catch(e) {}
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_flash_message.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_flash_message.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/shared/_flash_message.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_flash_message.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_footer.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_footer.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/shared/_footer.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_footer.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_header.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_header.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/shared/_header.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_header.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_job_details_breadcrumbs.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_job_details_breadcrumbs.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/shared/_job_details_breadcrumbs.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_job_details_breadcrumbs.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/shared/_package_material_revision_comment.ftl b/server/src/main/webapp/WEB-INF/vm/shared/_package_material_revision_comment.ftlh
similarity index 100%
rename from server/src/main/webapp/WEB-INF/vm/shared/_package_material_revision_comment.ftl
rename to server/src/main/webapp/WEB-INF/vm/shared/_package_material_revision_comment.ftlh
diff --git a/server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftl b/server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftlh
similarity index 95%
rename from server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftl
rename to server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftlh
index 35dc06ae03f..131f4c617b4 100644
--- a/server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftl
+++ b/server/src/main/webapp/WEB-INF/vm/sidebar/_sidebar_build_list.ftlh
@@ -35,11 +35,11 @@
             </#list>
         </ul>
     </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() {
           jQuery(".buildlist .time_ago").each(function(idx, timeSpan) {
             var timestamp = parseInt(jQuery(timeSpan).attr("data"));
diff --git a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/AbstractFreemarkerTemplateTest.java b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/AbstractFreemarkerTemplateTest.java
similarity index 82%
rename from server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/AbstractFreemarkerTemplateTest.java
rename to server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/AbstractFreemarkerTemplateTest.java
index fc7d7bdfce5..20d582ca395 100644
--- a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/AbstractFreemarkerTemplateTest.java
+++ b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/AbstractFreemarkerTemplateTest.java
@@ -12,23 +12,28 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * 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.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
     private SecurityService securityService;
     @Mock
@@ -49,6 +54,12 @@ public class AbstractFreemarkerTemplateTest {
         lenient().doReturn(versionInfoService).when(view).getVersionInfoService();
         lenient().doReturn(webpackAssetsService).when(view).webpackAssetsService();
         lenient().doReturn(securityService).when(view).getSecurityService();
         lenient().doReturn(maintenanceModeService).when(view).getMaintenanceModeService();
     }
+
+    @AfterEach
+    public void checkParseErrors() {
+        assertThat(parser.isTrackErrors()).isTrue();
+        assertThat(parser.getErrors()).isEmpty();
+    }
 }
diff --git a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/BuildDetailPageFreeMarkerTemplateTest.java b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/BuildDetailPageFreeMarkerTemplateTest.java
similarity index 54%
rename from server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/BuildDetailPageFreeMarkerTemplateTest.java
rename to server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/BuildDetailPageFreeMarkerTemplateTest.java
index 0ca27879307..44fc6005933 100644
--- a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/BuildDetailPageFreeMarkerTemplateTest.java
+++ b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/BuildDetailPageFreeMarkerTemplateTest.java
@@ -11,25 +11,26 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * 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;
 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;
 
 import java.util.Date;
@@ -39,55 +40,89 @@ import java.util.Map;
 import static com.thoughtworks.go.domain.buildcause.BuildCause.createWithModifications;
 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
     public void shouldRenderIframeSandboxForTestsTab() {
         JobDetailPresentationModel jobDetailPresentationModel = mock(JobDetailPresentationModel.class, RETURNS_SMART_NULLS);
         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() {
         GitMaterialConfig gitMaterialConfig = gitMaterialConfig();
 
         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);
     }
 
     private static Map<String, Object> minimalModelFrom(JobDetailPresentationModel jobDetailPresentationModel) {
diff --git a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/ExceptionTemplateTest.java b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/ExceptionTemplateTest.java
similarity index 60%
rename from server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/ExceptionTemplateTest.java
rename to server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/ExceptionTemplateTest.java
index a85ce2701cf..02c416a23b7 100644
--- a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/ExceptionTemplateTest.java
+++ b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/ExceptionTemplateTest.java
@@ -11,23 +11,22 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * 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;
 
 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 {
         super.setUp(TEMPLATE_PATH);
     }
@@ -36,14 +35,23 @@ public class ExceptionTemplateTest extends AbstractFreemarkerTemplateTest {
     public void shouldHaveErrorMessageAlongWithCustomMessageInOutputOfTemplate() {
         Map<String, Object> data = new HashMap<>();
         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.\");");
     }
 }
diff --git a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/TestFreeMarkerView.java b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/TestFreeMarkerView.java
similarity index 90%
rename from server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/TestFreeMarkerView.java
rename to server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/TestFreeMarkerView.java
index f1ea27ec469..5010d439364 100644
--- a/server/src/test-fast/java/com/thoughtworks/go/server/view/velocity/TestFreeMarkerView.java
+++ b/server/src/test-fast/java/com/thoughtworks/go/server/view/freemarker/TestFreeMarkerView.java
@@ -11,31 +11,36 @@
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * 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;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockServletContext;
 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 {
 
     private static final String REQUEST_CONTEXT_ATTRIBUTE = "req";
 
     public TestFreeMarkerView(String template) throws Exception {
         FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
         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);
         setUrl(template);
     }

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);