Skip to content

Commit 35e2736

Browse files
jtnorddaniel-beck
authored andcommitted
[SECURITY-2881]
1 parent a3a087b commit 35e2736

File tree

3 files changed

+177
-2
lines changed

3 files changed

+177
-2
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ target
22
work
33
*.iml
44
.idea
5-
5+
/.classpath
6+
/.project
7+
/.settings/
68
# macOS
79
.DS_Store

src/main/java/org/jenkinsci/plugins/workflow/support/steps/input/POSTHyperlinkNote.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
import hudson.console.ConsoleAnnotationDescriptor;
2929
import hudson.console.HyperlinkNote;
3030
import java.io.IOException;
31+
import java.io.UnsupportedEncodingException;
32+
import java.net.URLEncoder;
33+
import java.nio.charset.StandardCharsets;
34+
import java.util.Base64;
3135
import java.util.logging.Level;
3236
import java.util.logging.Logger;
3337
import jenkins.model.Jenkins;
@@ -80,7 +84,21 @@ public POSTHyperlinkNote(String url, int length) {
8084

8185
@Override protected String extraAttributes() {
8286
// TODO perhaps add hoverNotification
83-
return " onclick=\"new Ajax.Request('" + url + "'); return false\"";
87+
return " onclick=\"new Ajax.Request(decodeURIComponent(atob('" + encodeForJavascript(url) + "'))); return false\"";
88+
}
89+
90+
/**
91+
* Encode the String (using URLEncoding and then base64 encoding) so we can safely pass it to javascript where it can be decoded safely.
92+
* Javascript strings are UTF-16 and the endianness depends on the platform so we use URL encoding to ensure the String is all 7bit clean ascii and base64 encoding to fix passing any "unsafe" characters.
93+
*/
94+
private static String encodeForJavascript(String str) {
95+
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
96+
try {
97+
String encode = URLEncoder.encode(str, StandardCharsets.UTF_8.name());
98+
return Base64.getUrlEncoder().encodeToString(encode.getBytes(StandardCharsets.UTF_8));
99+
} catch (UnsupportedEncodingException e) {
100+
throw new InternalError("UTF-8 is missing but mandated by the JVM specification", e);
101+
}
84102
}
85103

86104
// TODO why does there need to be a descriptor at all?
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package org.jenkinsci.plugins.workflow.test.steps.input;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.allOf;
5+
import static org.hamcrest.Matchers.hasProperty;
6+
import static org.hamcrest.Matchers.is;
7+
import static org.hamcrest.Matchers.notNullValue;
8+
import java.net.URL;
9+
import java.util.Collections;
10+
import java.util.Map;
11+
import java.util.Set;
12+
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
13+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
14+
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
15+
import org.jenkinsci.plugins.workflow.steps.Step;
16+
import org.jenkinsci.plugins.workflow.steps.StepContext;
17+
import org.jenkinsci.plugins.workflow.steps.StepDescriptor;
18+
import org.jenkinsci.plugins.workflow.steps.StepExecution;
19+
import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution;
20+
import org.jenkinsci.plugins.workflow.support.steps.input.POSTHyperlinkNote;
21+
import org.junit.Ignore;
22+
import org.junit.Rule;
23+
import org.junit.Test;
24+
import org.jvnet.hudson.test.Issue;
25+
import org.jvnet.hudson.test.JenkinsRule;
26+
import org.jvnet.hudson.test.JenkinsRule.WebClient;
27+
import org.jvnet.hudson.test.TestExtension;
28+
import org.kohsuke.stapler.DataBoundConstructor;
29+
import com.gargoylesoftware.htmlunit.HttpMethod;
30+
import com.gargoylesoftware.htmlunit.MockWebConnection;
31+
import com.gargoylesoftware.htmlunit.Page;
32+
import com.gargoylesoftware.htmlunit.WebRequest;
33+
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
34+
import com.gargoylesoftware.htmlunit.html.HtmlPage;
35+
import edu.umd.cs.findbugs.annotations.NonNull;
36+
import hudson.model.ParametersAction;
37+
import hudson.model.ParametersDefinitionProperty;
38+
import hudson.model.Result;
39+
import hudson.model.StringParameterDefinition;
40+
import hudson.model.StringParameterValue;
41+
import hudson.model.TaskListener;
42+
import hudson.model.queue.QueueTaskFuture;
43+
import jenkins.model.Jenkins;
44+
45+
public class POSTHyperlinkNoteTest {
46+
47+
@Rule
48+
public JenkinsRule jr = new JenkinsRule();
49+
50+
@Test
51+
@Issue("SECURITY-2881")
52+
public void urlsAreSafeFromJavascriptInjection() throws Exception {
53+
testSanitization("whatever/'+alert(1)+'");
54+
}
55+
56+
@Test
57+
@Ignore("webclient does not support unicode URLS and this is passed as /jenkins/whatever/%F0%9F%99%88%F0%9F%99%89%F0%9F%99%8A%F0%9F%98%80%E2%98%BA")
58+
public void testPassingMultiByteCharacters() throws Exception {
59+
// this is actually illegal in HTML4 but common -> https://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars
60+
// browsers infer the URL from the charset and then encode the escaped characters...
61+
testSanitization("whatever/🙈🙉🙊😀☺");
62+
}
63+
64+
@Test
65+
public void testPassingSingleByte() throws Exception {
66+
testSanitization("whatever/something?withparameter=baa");
67+
}
68+
69+
void testSanitization(String fragment) throws Exception {
70+
WorkflowJob project = jr.createProject(WorkflowJob.class);
71+
project.setDefinition(new CpsFlowDefinition("security2881(params.TEST_URL)\n", true));
72+
project.addProperty(new ParametersDefinitionProperty(new StringParameterDefinition("TEST_URL", "WHOOPS")));
73+
74+
QueueTaskFuture<WorkflowRun> scheduleBuild = project.scheduleBuild2(0, new ParametersAction(new StringParameterValue("TEST_URL", fragment)));
75+
WorkflowRun run = jr.assertBuildStatus(Result.SUCCESS, scheduleBuild);
76+
WebClient wc = jr.createWebClient();
77+
78+
HtmlPage page = wc.getPage(run, "console");
79+
HtmlAnchor anchor = page.getAnchorByText("SECURITY-2881");
80+
assertThat(anchor, notNullValue());
81+
MockWebConnection mwc = new MockWebConnection();
82+
mwc.setDefaultResponse("<html><body>Hello</body></html>");
83+
wc.setWebConnection(mwc);
84+
System.out.println(anchor);
85+
Page p = anchor.click();
86+
87+
// the click executes an ajax request - and so we need to wait until that has completed
88+
// ideally we would pass zero here as we have already clicked the javascript should have
89+
// started executing - but this is not always the case
90+
wc.waitForBackgroundJavaScriptStartingBefore(500);
91+
92+
// check we have an interaction at the correct place and its not a javascript issue.
93+
WebRequest request = mwc.getLastWebRequest();
94+
assertThat(request, notNullValue());
95+
assertThat(request.getHttpMethod(), is(HttpMethod.POST));
96+
URL url = request.getUrl();
97+
System.out.println(url.toExternalForm());
98+
assertThat(url, allOf(hasProperty("host", is(new URL(jr.jenkins.getConfiguredRootUrl()).getHost())),
99+
hasProperty("file", is(jr.contextPath + '/' + fragment))));
100+
}
101+
102+
public static class Security2881ConsoleStep extends Step {
103+
104+
private final String urlFragment;
105+
106+
@DataBoundConstructor
107+
public Security2881ConsoleStep(String urlFragment) {
108+
this.urlFragment = urlFragment;
109+
}
110+
111+
@Override
112+
public StepExecution start(StepContext context) throws Exception {
113+
return new Security2881ConsoleStepExecution(context, urlFragment);
114+
}
115+
116+
@TestExtension
117+
public static final class DescriptorImpl extends StepDescriptor {
118+
119+
@Override public String getFunctionName() {
120+
return "security2881";
121+
}
122+
123+
@NonNull
124+
@Override public String getDisplayName() {
125+
return "Security2881";
126+
}
127+
128+
@Override public Set<? extends Class<?>> getRequiredContext() {
129+
return Collections.singleton(TaskListener.class);
130+
}
131+
132+
@Override public String argumentsToString(@NonNull Map<String, Object> namedArgs) {
133+
return null;
134+
}
135+
}
136+
137+
public static class Security2881ConsoleStepExecution extends SynchronousStepExecution<Void> {
138+
139+
private final String urlFragment;
140+
141+
protected Security2881ConsoleStepExecution(StepContext context, String urlFragment) {
142+
super(context);
143+
this.urlFragment = urlFragment;
144+
}
145+
146+
@Override
147+
protected Void run() throws Exception {
148+
TaskListener taskListener = getContext().get(TaskListener.class);
149+
// use the same URL for CORS.
150+
taskListener.getLogger().print(POSTHyperlinkNote.encodeTo(Jenkins.get().getConfiguredRootUrl() + urlFragment, "SECURITY-2881"));
151+
return null;
152+
}
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)