Skip to content

Commit

Permalink
SECURITY-3302
Browse files Browse the repository at this point in the history
  • Loading branch information
BorisYaoA authored and raul-arabaolaza committed Feb 28, 2024
1 parent 6b84024 commit c0eed94
Show file tree
Hide file tree
Showing 13 changed files with 494 additions and 10 deletions.
10 changes: 6 additions & 4 deletions src/main/java/htmlpublisher/HtmlPublisher.java
Expand Up @@ -81,6 +81,8 @@

import edu.umd.cs.findbugs.annotations.NonNull;

import static hudson.Functions.htmlAttributeEscape;


/**
* Saves HTML reports for the project and publishes them.
Expand Down Expand Up @@ -130,7 +132,7 @@ private static String writeFile(List<String> lines, File path) throws IOExceptio
return Util.toHexString(sha1.digest());
}

public List<String> readFile(String filePath) throws
public List<String> readFile(String filePath) throws
java.io.IOException {
return readFile(filePath, this.getClass());
}
Expand Down Expand Up @@ -302,7 +304,7 @@ public static boolean publishReports(Run<?, ?> build, FilePath workspace, TaskLi
// On windows file paths contains back slashes, but
// in the HTML file we do not want them, so replace them with forward slash
report = report.replace("\\", "/");

// Ignore blank report names caused by trailing or double commas.
if (report.isEmpty()) {
continue;
Expand All @@ -318,13 +320,13 @@ public static boolean publishReports(Run<?, ?> build, FilePath workspace, TaskLi
} else {
reportFile = report;
}
String tabItem = "<li id=\"" + tabNo + "\" class=\"unselected\" onclick=\"updateBody('" + tabNo + "');\" value=\"" + report + "\">" + getTitle(reportFile, titles, j) + "</li>";
String tabItem = "<li id=\"" + tabNo + "\" class=\"unselected\" onclick=\"updateBody('" + tabNo + "');\" value=\"" + htmlAttributeEscape(report) + "\">" + htmlAttributeEscape(getTitle(reportFile, titles, j)) + "</li>";
reportLines.add(tabItem);
}
// Add the JS to change the link as appropriate.
String hudsonUrl = Jenkins.get().getRootUrl();
Job job = build.getParent();
reportLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").innerHTML=\"Back to " + job.getName() + "\";</script>");
reportLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").innerHTML=\"Back to " + htmlAttributeEscape(job.getName()) + "\";</script>");
// If the URL isn't configured in Hudson, the best we can do is attempt to go Back.
if (hudsonUrl == null) {
reportLines.add("<script type=\"text/javascript\">document.getElementById(\"hudson_link\").onclick = function() { history.go(-1); return false; };</script>");
Expand Down
Expand Up @@ -3,6 +3,7 @@ package htmlpublisher.HtmlPublisherTarget.BaseHTMLAction
import htmlpublisher.HtmlPublisher
import htmlpublisher.HtmlPublisherTarget
import hudson.Util
import hudson.model.Descriptor

import java.security.MessageDigest

Expand Down Expand Up @@ -57,6 +58,30 @@ def serveWrapperLegacyDirectly() {

def legacyFile = new File(my.dir(), "htmlpublisher-wrapper.html")

def scriptPattern = legacyFile.text =~ /(<script type="text\/javascript">document.getElementById\("hudson_link"\).innerHTML="Back to )(.*[<>"\\].*)(";<\/script>)/

if (scriptPattern.find()) {
throw new Descriptor.FormException("Can't use illegal character in the Job Name", "JobName")
}

def tabPattern = legacyFile.text =~ /(<li id="tab\d+" class="unselected" onclick="updateBody\('tab\d+'\);" value=")(.*[<>"\\].*)(">)(.*[<>"\\].*)(<\/li>)/

if (tabPattern.find()) {
throw new Descriptor.FormException("Can't use illegal character in the Report Name", "ReportName")
}

def valuePattern = legacyFile.text =~ /(<li id="tab\d+" class="unselected" onclick="updateBody\('tab\d+'\);" value=")([^<]+)(">)(.*[<>"\\].*)(<\/li>)/

if (valuePattern.find()) {
throw new Descriptor.FormException("Can't use illegal character in the Report Name", "ReportName")
}

def titlePattern = legacyFile.text =~ /(<li id="tab\d+" class="unselected" onclick="updateBody\('tab\d+'\);" value=")(.*[<>"\\].*)(">)([^<]+)(<\/li>)/

if (titlePattern.find()) {
throw new Descriptor.FormException("Can't use illegal character in the Report Name", "ReportName")
}

raw(legacyFile.text)
}

Expand Down
12 changes: 6 additions & 6 deletions src/test/java/htmlpublisher/HtmlFileNameTest.java
Expand Up @@ -24,7 +24,7 @@ public void fileNameWithSpecialCharactersAndSingleSlash() throws Exception {

FreeStyleProject job = j.createFreeStyleProject();

job.getBuildersList().add(new CreateFileBuilder("subdir/#$&+,;= @.html", content));
job.getBuildersList().add(new CreateFileBuilder("subdir/#$+,;= @.html", content));
job.getPublishersList().add(new HtmlPublisher(Arrays.asList(
new HtmlPublisherTarget("report-name", "", "subdir/*.html", true, true, false))));
job.save();
Expand All @@ -33,12 +33,12 @@ public void fileNameWithSpecialCharactersAndSingleSlash() throws Exception {

JenkinsRule.WebClient client = j.createWebClient();
assertEquals(content,
client.getPage(job, "report-name/subdir/%23%24%26%2B%2C%3B%3D%20%40.html").getWebResponse().getContentAsString());
client.getPage(job, "report-name/subdir/%23%24%2B%2C%3B%3D%20%40.html").getWebResponse().getContentAsString());

// published html page(s)
HtmlPage page = client.getPage(job, "report-name");
HtmlInlineFrame iframe = (HtmlInlineFrame) page.getElementById("myframe");
assertEquals("subdir/%23%24%26%2B%2C%3B%3D%20%40.html", iframe.getAttribute("src"));
assertEquals("subdir/%23%24%2B%2C%3B%3D%20%40.html", iframe.getAttribute("src"));

HtmlPage pageInIframe = (HtmlPage) iframe.getEnclosedPage();
assertEquals("Hello world!", pageInIframe.getBody().asNormalizedText());
Expand All @@ -50,7 +50,7 @@ public void fileNameWithSpecialCharactersAndMultipleSlashes() throws Exception {

FreeStyleProject job = j.createFreeStyleProject();

job.getBuildersList().add(new CreateFileBuilder("subdir/subdir2/#$&+,;= @.html", content));
job.getBuildersList().add(new CreateFileBuilder("subdir/subdir2/#$+,;= @.html", content));
job.getPublishersList().add(new HtmlPublisher(Arrays.asList(
new HtmlPublisherTarget("report-name", "", "subdir/subdir2/*.html", true, true, false))));
job.save();
Expand All @@ -59,12 +59,12 @@ public void fileNameWithSpecialCharactersAndMultipleSlashes() throws Exception {

JenkinsRule.WebClient client = j.createWebClient();
assertEquals(content,
client.getPage(job, "report-name/subdir/subdir2/%23%24%26%2B%2C%3B%3D%20%40.html").getWebResponse().getContentAsString());
client.getPage(job, "report-name/subdir/subdir2/%23%24%2B%2C%3B%3D%20%40.html").getWebResponse().getContentAsString());

// published html page(s)
HtmlPage page = client.getPage(job, "report-name");
HtmlInlineFrame iframe = (HtmlInlineFrame) page.getElementById("myframe");
assertEquals("subdir/subdir2/%23%24%26%2B%2C%3B%3D%20%40.html", iframe.getAttribute("src"));
assertEquals("subdir/subdir2/%23%24%2B%2C%3B%3D%20%40.html", iframe.getAttribute("src"));

HtmlPage pageInIframe = (HtmlPage) iframe.getEnclosedPage();
assertEquals("Hello world!", pageInIframe.getBody().asNormalizedText());
Expand Down
218 changes: 218 additions & 0 deletions src/test/java/htmlpublisher/Security3302Test.java
@@ -0,0 +1,218 @@
package htmlpublisher;

import hudson.model.FreeStyleProject;
import hudson.tasks.Shell;
import org.htmlunit.AlertHandler;
import org.htmlunit.FailingHttpStatusCodeException;
import org.htmlunit.Page;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static hudson.Functions.isWindows;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeFalse;

public class Security3302Test {

@Rule
public JenkinsRule j = new JenkinsRule();

@Test
public void security3302sanitizeJobNameTest() throws Exception {

// Skip on windows
assumeFalse(isWindows());

FreeStyleProject job = j.jenkins.createProject(FreeStyleProject.class, "\"+alert(1)+\"");
job.getBuildersList().add(new Shell("date > index.html"));

HtmlPublisherTarget target = new HtmlPublisherTarget(
"HTML Report",
"",
"index.html",
true,
false,
false
);

target.setUseWrapperFileDirectly(true);
target.setEscapeUnderscores(true);
target.setReportTitles("");
target.setIncludes("**/*");

List<HtmlPublisherTarget> reportTargets = new ArrayList<>();
reportTargets.add(target);

job.getPublishersList().add(new HtmlPublisher(reportTargets));

j.buildAndAssertSuccess(job);

HtmlPublisherTarget.HTMLAction action = job.getAction(HtmlPublisherTarget.HTMLAction.class);
assertNotNull(action);

assertEquals("HTML Report", action.getHTMLTarget().getReportName());
assertEquals("HTML_20Report", action.getUrlName());

JenkinsRule.WebClient client = j.createWebClient();

// Create an alert handler to check for any alerts
Alerter alerter = new Alerter();
client.setAlertHandler(alerter);
client.goTo("job/\"+alert(1)+\"/HTML_20Report/");

// Check that the alerter has not been triggered
client.waitForBackgroundJavaScript(2000);
assertTrue(alerter.messages.isEmpty());

}

@Test
@LocalData
@Issue("security-3302")
public void oldReportJobNameTest() throws Exception {
// Skip on windows
assumeFalse(isWindows());
List<FreeStyleProject> items = j.jenkins.getItems(FreeStyleProject.class);
assertThat(items, not(empty()));
FreeStyleProject job = items.get(0);
assertNotNull(job);
HtmlPublisherTarget.HTMLAction action = job.getAction(HtmlPublisherTarget.HTMLAction.class);
assertNotNull(action);

assertEquals("HTML Report", action.getHTMLTarget().getReportName());
assertEquals("HTML_20Report", action.getUrlName());

JenkinsRule.WebClient client = j.createWebClient();

// Create an alert handler to check for any alerts
Alerter alerter = new Alerter();
client.setAlertHandler(alerter);

try {
client.goTo("job/testJob/1/HTML_20Report/");

} catch (FailingHttpStatusCodeException e) {
// Ignore the exception as needed
} finally {

client.waitForBackgroundJavaScript(2000);
assertTrue(alerter.messages.isEmpty());
}
}

@Test
public void security3302sanitizeOptionalNameTest() throws Exception {

// Skip on windows
assumeFalse(isWindows());

FreeStyleProject job = j.jenkins.createProject(FreeStyleProject.class, "testJob");
job.getBuildersList().add(new Shell("echo \"Test\" > test.txt"));

HtmlPublisherTarget target = new HtmlPublisherTarget(
"HTML Report",
"",
"test.txt",
true,
false,
false
);

target.setUseWrapperFileDirectly(true);
target.setEscapeUnderscores(true);
target.setReportTitles("<img src onerror=alert(1)>");
target.setIncludes("**/*");

List<HtmlPublisherTarget> reportTargets = new ArrayList<>();
reportTargets.add(target);

job.getPublishersList().add(new HtmlPublisher(reportTargets));

j.buildAndAssertSuccess(job);

HtmlPublisherTarget.HTMLAction action = job.getAction(HtmlPublisherTarget.HTMLAction.class);
assertNotNull(action);

assertEquals("HTML Report", action.getHTMLTarget().getReportName());
assertEquals("HTML_20Report", action.getUrlName());

JenkinsRule.WebClient client = j.createWebClient();

// Create an alert handler to check for any alerts
Alerter alerter = new Alerter();
client.setAlertHandler(alerter);
client.goTo("job/testJob/HTML_20Report/");

// Check that the alerter has not been triggered
client.waitForBackgroundJavaScript(2000);
assertTrue(alerter.messages.isEmpty());

}

@Test
public void security3302sanitizeExistingReportTitleTest() throws Exception {

// Skip on windows
assumeFalse(isWindows());

FreeStyleProject job = j.jenkins.createProject(FreeStyleProject.class, "testJob");
job.getBuildersList().add(new Shell("echo \"Test\" > '\"><img src onerror=alert(1)>'"));

HtmlPublisherTarget target = new HtmlPublisherTarget(
"HTML Report",
"",
"",
true,
false,
false
);

target.setUseWrapperFileDirectly(true);
target.setEscapeUnderscores(true);
target.setReportTitles("\"><img src onerror=alert(1)>");
target.setIncludes("**/*");

List<HtmlPublisherTarget> reportTargets = new ArrayList<>();
reportTargets.add(target);

job.getPublishersList().add(new HtmlPublisher(reportTargets));

j.buildAndAssertSuccess(job);

HtmlPublisherTarget.HTMLAction action = job.getAction(HtmlPublisherTarget.HTMLAction.class);
assertNotNull(action);

assertEquals("HTML Report", action.getHTMLTarget().getReportName());
assertEquals("HTML_20Report", action.getUrlName());

JenkinsRule.WebClient client = j.createWebClient();

Alerter alerter = new Alerter();
client.setAlertHandler(alerter);
client.goTo("job/testJob/HTML_20Report/");

// Check that the alerter has not been triggered
client.waitForBackgroundJavaScript(2000);
assertTrue(alerter.messages.isEmpty());

}

// This class is used to check for any alerts that are triggered on a page
static class Alerter implements AlertHandler {
List<String> messages = Collections.synchronizedList(new ArrayList<>());
@Override
public void handleAlert(final Page page, final String message) {
messages.add(message);
}
}
}
@@ -0,0 +1,44 @@
<?xml version='1.1' encoding='UTF-8'?>
<build>
<actions>
<hudson.model.CauseAction>
<causeBag class="linked-hash-map">
<entry>
<hudson.model.Cause_-UserIdCause/>
<int>1</int>
</entry>
</causeBag>
</hudson.model.CauseAction>
<htmlpublisher.HtmlPublisherTarget_-HTMLBuildAction plugin="htmlpublisher@1.33-SNAPSHOT">
<actualHtmlPublisherTarget>
<reportName>HTML Report</reportName>
<reportDir></reportDir>
<reportFiles>index.html</reportFiles>
<alwaysLinkToLastBuild>false</alwaysLinkToLastBuild>
<reportTitles></reportTitles>
<keepAll>true</keepAll>
<allowMissing>false</allowMissing>
<includes>**/*</includes>
<escapeUnderscores>true</escapeUnderscores>
<useWrapperFileDirectly>true</useWrapperFileDirectly>
</actualHtmlPublisherTarget>
<outer-class reference="../actualHtmlPublisherTarget"/>
<wrapperChecksum>bb013837dd6fed1ea7ef00d584484d62e90b64a1</wrapperChecksum>
<outer-class defined-in="htmlpublisher.HtmlPublisherTarget$HTMLBuildAction" reference="../actualHtmlPublisherTarget"/>
</htmlpublisher.HtmlPublisherTarget_-HTMLBuildAction>
</actions>
<queueId>1</queueId>
<timestamp>1702036826488</timestamp>
<startTime>1702036826496</startTime>
<result>SUCCESS</result>
<duration>99</duration>
<charset>UTF-8</charset>
<keepLog>false</keepLog>
<builtOn></builtOn>
<workspace>workspace/&quot;+alert(1)+&quot;</workspace>
<hudsonVersion>2.387.3</hudsonVersion>
<scm class="hudson.scm.NullChangeLogParser"/>
<culprits class="java.util.Collections$UnmodifiableSet">
<c class="sorted-set"/>
</culprits>
</build>
@@ -0,0 +1 @@
<log/>

0 comments on commit c0eed94

Please sign in to comment.