Skip to content

Commit 3de4d8f

Browse files
authored
Merge pull request #4302 from daniel-beck/JENKINS-59849
[JENKINS-59849] Don't fail to serve resource files with nontrivial names
2 parents 11c1146 + a2477f6 commit 3de4d8f

File tree

2 files changed

+100
-5
lines changed

2 files changed

+100
-5
lines changed

core/src/main/java/jenkins/security/ResourceDomainRootAction.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
*/
2424
package jenkins.security;
2525

26+
import com.google.common.annotations.VisibleForTesting;
2627
import hudson.Extension;
2728
import hudson.ExtensionList;
2829
import hudson.Util;
@@ -51,6 +52,7 @@
5152
import java.util.Base64;
5253
import java.util.logging.Level;
5354
import java.util.logging.Logger;
55+
import java.util.stream.Collectors;
5456

5557
import static java.time.Instant.*;
5658
import static java.time.temporal.ChronoUnit.MINUTES;
@@ -147,7 +149,7 @@ public String getRedirectUrl(@Nonnull Token token, @Nonnull String restOfPath) {
147149
// Unsure whether this can happen -- just be safe here
148150
restOfPath = "/" + restOfPath;
149151
}
150-
return resourceRootUrl + getUrlName() + "/" + token.encode() + restOfPath;
152+
return resourceRootUrl + getUrlName() + "/" + token.encode() + Arrays.stream(restOfPath.split("[/]")).map(Util::rawEncode).collect(Collectors.joining("/"));
151153
}
152154

153155
private static String getResourceRootUrl() {
@@ -165,7 +167,7 @@ private static String getResourceRootUrl() {
165167
@CheckForNull
166168
public Token getToken(@Nonnull DirectoryBrowserSupport dbs, @Nonnull StaplerRequest req) {
167169
// This is the "restOfPath" of the DirectoryBrowserSupport, i.e. the directory/file/pattern "inside" the DBS.
168-
final String dbsFile = req.getRestOfPath();
170+
final String dbsFile = req.getOriginalRestOfPath();
169171

170172
// Now get the 'restOfUrl' after the top-level ancestor (which is the Jenkins singleton).
171173
// In other words, this is the complete URL after Jenkins handled the top-level request.
@@ -222,7 +224,8 @@ public void doDynamic(StaplerRequest req, StaplerResponse rsp) throws IOExceptio
222224

223225
try (ACLContext ignored = ACL.as(auth)) {
224226
try {
225-
Stapler.getCurrent().invoke(req, rsp, Jenkins.get(), requestUrlSuffix + restOfPath);
227+
String path = requestUrlSuffix + Arrays.stream(restOfPath.split("[/]")).map(Util::rawEncode).collect(Collectors.joining("/"));
228+
Stapler.getCurrent().invoke(req, rsp, Jenkins.get(), path);
226229
} catch (Exception ex) {
227230
// cf. UnwrapSecurityExceptionFilter
228231
Throwable cause = ex.getCause();
@@ -263,7 +266,9 @@ public static class Token {
263266
private String path;
264267
private String username;
265268
private Instant timestamp;
266-
private Token (String path, @Nullable String username, Instant timestamp) {
269+
270+
@VisibleForTesting
271+
Token (String path, @Nullable String username, Instant timestamp) {
267272
this.path = path;
268273
this.username = Util.fixNull(username);
269274
this.timestamp = timestamp;
@@ -277,8 +282,8 @@ private String encode() {
277282
}
278283

279284
private static Token decode(String value) {
280-
byte[] byteValue = Base64.getUrlDecoder().decode(value);
281285
try {
286+
byte[] byteValue = Base64.getUrlDecoder().decode(value);
282287
byte[] mac = Arrays.copyOf(byteValue, 32);
283288
byte[] restBytes = Arrays.copyOfRange(byteValue, 32, byteValue.length);
284289
String rest = new String(restBytes, StandardCharsets.UTF_8);

test/src/test/java/jenkins/security/ResourceDomainTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import com.gargoylesoftware.htmlunit.Page;
44
import com.gargoylesoftware.htmlunit.html.HtmlPage;
55
import hudson.ExtensionList;
6+
import hudson.FilePath;
67
import hudson.model.DirectoryBrowserSupport;
78
import hudson.model.FreeStyleProject;
89
import hudson.model.Item;
10+
import hudson.model.UnprotectedRootAction;
911
import jenkins.model.Jenkins;
1012
import jenkins.model.JenkinsLocationConfiguration;
1113
import org.junit.Assert;
@@ -17,8 +19,12 @@
1719
import org.jvnet.hudson.test.Issue;
1820
import org.jvnet.hudson.test.JenkinsRule;
1921
import org.jvnet.hudson.test.MockAuthorizationStrategy;
22+
import org.jvnet.hudson.test.TestExtension;
23+
import org.kohsuke.stapler.HttpResponse;
2024

25+
import javax.annotation.CheckForNull;
2126
import java.net.URL;
27+
import java.time.Instant;
2228
import java.util.UUID;
2329

2430
@Issue("JENKINS-41891")
@@ -279,4 +285,88 @@ public void adminMonitorShowsUpWithOverriddenCSP() throws Exception {
279285
}
280286
Assert.assertFalse(monitor.isActivated());
281287
}
288+
289+
@Test
290+
public void testRedirectUrls() throws Exception {
291+
ResourceDomainRootAction rootAction = ResourceDomainRootAction.get();
292+
String url = rootAction.getRedirectUrl(new ResourceDomainRootAction.Token("foo", "bar", Instant.now()), "foo bar baz");
293+
Assert.assertFalse("urlencoded", url.contains(" "));
294+
}
295+
296+
@Test
297+
@Issue("JENKINS-59849")
298+
public void testUrlEncoding() throws Exception {
299+
FreeStyleProject project = j.createFreeStyleProject();
300+
project.getBuildersList().add(new CreateFileBuilder("This has spaces and is 100% evil.html", "<html><body>the content</body></html>"));
301+
project.save();
302+
303+
j.buildAndAssertSuccess(project);
304+
305+
JenkinsRule.WebClient webClient = j.createWebClient();
306+
webClient.setThrowExceptionOnFailingStatusCode(false);
307+
webClient.setRedirectEnabled(true);
308+
309+
HtmlPage page = webClient.getPage(project, "ws/This%20has%20spaces%20and%20is%20100%25%20evil.html");
310+
Assert.assertEquals("page is found", 200, page.getWebResponse().getStatusCode());
311+
Assert.assertTrue("page content is as expected", page.getWebResponse().getContentAsString().contains("the content"));
312+
313+
URL url = page.getUrl();
314+
Assert.assertTrue("page is served by resource domain", url.toString().contains("/static-files/"));
315+
}
316+
317+
@Test
318+
@Issue("JENKINS-59849")
319+
public void testMoreUrlEncoding() throws Exception {
320+
JenkinsRule.WebClient webClient = j.createWebClient();
321+
webClient.setThrowExceptionOnFailingStatusCode(false);
322+
webClient.setRedirectEnabled(true);
323+
324+
Page page = webClient.goTo("100%25%20evil/%20100%25%20evil%20dir%20name%20%20%20/%20100%25%20evil%20content%20.html");
325+
Assert.assertEquals("page is found", 200, page.getWebResponse().getStatusCode());
326+
Assert.assertTrue("page content is as expected", page.getWebResponse().getContentAsString().contains("this is the content"));
327+
328+
URL url = page.getUrl();
329+
Assert.assertTrue("page is served by resource domain", url.toString().contains("/static-files/"));
330+
331+
URL dirUrl = new URL(url.toString().replace("%20100%25%20evil%20content%20.html", ""));
332+
Page dirPage = webClient.getPage(dirUrl);
333+
Assert.assertEquals("page is found", 200, dirPage.getWebResponse().getStatusCode());
334+
Assert.assertTrue("page content is HTML", dirPage.getWebResponse().getContentAsString().contains("href"));
335+
Assert.assertTrue("page content references file", dirPage.getWebResponse().getContentAsString().contains("evil content"));
336+
337+
URL topDirUrl = new URL(url.toString().replace("%20100%25%20evil%20dir%20name%20%20%20/%20100%25%20evil%20content%20.html", ""));
338+
Page topDirPage = webClient.getPage(topDirUrl);
339+
Assert.assertEquals("page is found", 200, topDirPage.getWebResponse().getStatusCode());
340+
Assert.assertTrue("page content is HTML", topDirPage.getWebResponse().getContentAsString().contains("href"));
341+
Assert.assertTrue("page content references directory", topDirPage.getWebResponse().getContentAsString().contains("evil dir name"));
342+
}
343+
344+
@TestExtension
345+
public static class RootActionImpl implements UnprotectedRootAction {
346+
347+
@CheckForNull
348+
@Override
349+
public String getIconFileName() {
350+
return null;
351+
}
352+
353+
@CheckForNull
354+
@Override
355+
public String getDisplayName() {
356+
return null;
357+
}
358+
359+
@CheckForNull
360+
@Override
361+
public String getUrlName() {
362+
return "100% evil";
363+
}
364+
365+
public HttpResponse doDynamic() throws Exception {
366+
Jenkins jenkins = Jenkins.get();
367+
FilePath tempDir = jenkins.getRootPath().createTempDir("root", "tmp");
368+
tempDir.child(" 100% evil dir name ").child(" 100% evil content .html").write("this is the content", "UTF-8");
369+
return new DirectoryBrowserSupport(jenkins, tempDir, "title", "", true);
370+
}
371+
}
282372
}

0 commit comments

Comments
 (0)