Security context
Medium· 4.3GHSA-3rqh-hch3-jhpc CVE-2026-53436CWE-601Published Jun 10, 2026

Jenkins Open Redirect via Relative Path Segments in Post-Login Redirect URL

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

2.556 → fixed in 2.5680 → fixed in 2.555.3

Details

Jenkins 2.567 and earlier, LTS 2.555.2 and earlier improperly determines that a redirect URL after login is legitimately pointing to Jenkins when it contains relative path segments (`./` or `../`), allowing attackers to perform phishing attacks.

The fix

[SECURITY-3711][SECURITY-3755]

Kevin-CB· Jun 2, 2026, 10:14 AM+3298ef52891b0
core/src/main/java/hudson/Util.java+3 2
@@ -1703,11 +1703,12 @@ public static boolean isAbsoluteUri(@NonNull String uri) {
}
/**
- * Return true iff the parameter does not denote an absolute URI and not a scheme-relative URI.
+ * Return true if and only if the parameter does not denote an absolute URI and not a scheme-relative URI.
* @since 2.3 / 1.651.2
*/
public static boolean isSafeToRedirectTo(@NonNull String uri) {
- return !isAbsoluteUri(uri) && !uri.startsWith("\\") && !uri.replace('\\', '/').startsWith("//");
+ String normalized = uri.replace("\t", "").replace("\n", "").replace("\r", "");
+ return !isAbsoluteUri(normalized) && !normalized.startsWith("\\") && !normalized.replace('\\', '/').contains("//");
}
/**
core/src/test/java/hudson/UtilTest.java+14 1
@@ -415,7 +415,7 @@ void testIsAbsoluteUri() {
}
@Test
- @Issue({"SECURITY-276", "SECURITY-3501"})
+ @Issue({"SECURITY-276", "SECURITY-3501", "SECURITY-3711"})
void testIsSafeToRedirectTo() {
assertFalse(Util.isSafeToRedirectTo("http://foobar/"));
assertFalse(Util.isSafeToRedirectTo("mailto:kk@kohsuke.org"));
@@ -426,6 +426,19 @@ void testIsSafeToRedirectTo() {
assertFalse(Util.isSafeToRedirectTo("/\\google.com"));
assertFalse(Util.isSafeToRedirectTo("\\google.com"));
+ assertFalse(Util.isSafeToRedirectTo(".//google.com"));
+ assertFalse(Util.isSafeToRedirectTo(".///google.com"));
+ assertFalse(Util.isSafeToRedirectTo("aaa/..//google.com"));
+ assertFalse(Util.isSafeToRedirectTo("./aaa/..//google.com"));
+ assertFalse(Util.isSafeToRedirectTo("./\\/google.com"));
+
+ assertFalse(Util.isSafeToRedirectTo("/\t/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\n/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\r/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("\t//google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\t/\t/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\t\n\r/google.com"));
+
assertTrue(Util.isSafeToRedirectTo("foo/bar/abc:def"));
assertTrue(Util.isSafeToRedirectTo("foo?abc:def"));
assertTrue(Util.isSafeToRedirectTo("foo#abc:def"));
test/src/test/java/jenkins/security/Security3501Test.java+15 6
@@ -13,6 +13,7 @@
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.ValueSource;
+import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.junit.jupiter.RealJenkinsExtension;
@@ -31,6 +32,7 @@ void setUp() {
jj.withPrefix(contextPath);
}
+ @Issue({"SECURITY-3501", "SECURITY-3711"})
@Test
void testRedirects() throws Throwable {
jj.then(new TestRedirectsStep(contextPath));
@@ -38,20 +40,27 @@ void testRedirects() throws Throwable {
private record TestRedirectsStep(String context) implements RealJenkinsExtension.Step {
public void run(JenkinsRule j) throws Exception {
- List<String> prohibitedPaths = List.of("%5C%5Cexample.org", "%5C/example.org", "/%5Cexample.org", "//example.org", "https://example.org", "\\example.org");
+ List<String> prohibitedPaths = List.of(
+ // SECURITY-3501
+ "%5C%5Cexample.org", "%5C/example.org", "/%5Cexample.org", "//example.org",
+ "https://example.org", "\\example.org",
+ ".//example.org", ".///example.org", ".////example.org",
+ "aaa/..//example.org", "./aaa/..//example.org", "./%5C/example.org",
+ "/%09/example.org", "/%0A/example.org", "/%0D/example.org",
+ "%09//example.org", "/%09/%09/example.org");
for (String path : prohibitedPaths) {
try (JenkinsRule.WebClient wc = j.createWebClient().withRedirectEnabled(false)) {
- final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path));
- assertThat(fhsce.getStatusCode(), is(404));
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path), "path=" + path);
+ assertThat("path=" + path, fhsce.getStatusCode(), is(404));
}
}
List<String> allowedPaths = List.of("foo", "foo/bar");
for (String path : allowedPaths) {
try (JenkinsRule.WebClient wc = j.createWebClient().withRedirectEnabled(false)) {
- final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path));
- assertThat(fhsce.getStatusCode(), is(302));
- assertThat(fhsce.getResponse().getResponseHeaderValue("Location"), is(context + "/redirects/" + path));
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path), "path=" + path);
+ assertThat("path=" + path, fhsce.getStatusCode(), is(302));
+ assertThat("path=" + path, fhsce.getResponse().getResponseHeaderValue("Location"), is(context + "/redirects/" + path));
}
}
}

[SECURITY-3711][SECURITY-3755]

Kevin-CB· May 29, 2026, 07:39 AM+329b32f2f27a8
core/src/main/java/hudson/Util.java+3 2
@@ -1647,11 +1647,12 @@ public static boolean isAbsoluteUri(@NonNull String uri) {
}
/**
- * Return true iff the parameter does not denote an absolute URI and not a scheme-relative URI.
+ * Return true if and only if the parameter does not denote an absolute URI and not a scheme-relative URI.
* @since 2.3 / 1.651.2
*/
public static boolean isSafeToRedirectTo(@NonNull String uri) {
- return !isAbsoluteUri(uri) && !uri.startsWith("\\") && !uri.replace('\\', '/').startsWith("//");
+ String normalized = uri.replace("\t", "").replace("\n", "").replace("\r", "");
+ return !isAbsoluteUri(normalized) && !normalized.startsWith("\\") && !normalized.replace('\\', '/').contains("//");
}
/**
core/src/test/java/hudson/UtilTest.java+14 1
@@ -415,7 +415,7 @@ void testIsAbsoluteUri() {
}
@Test
- @Issue({"SECURITY-276", "SECURITY-3501"})
+ @Issue({"SECURITY-276", "SECURITY-3501", "SECURITY-3711"})
void testIsSafeToRedirectTo() {
assertFalse(Util.isSafeToRedirectTo("http://foobar/"));
assertFalse(Util.isSafeToRedirectTo("mailto:kk@kohsuke.org"));
@@ -426,6 +426,19 @@ void testIsSafeToRedirectTo() {
assertFalse(Util.isSafeToRedirectTo("/\\google.com"));
assertFalse(Util.isSafeToRedirectTo("\\google.com"));
+ assertFalse(Util.isSafeToRedirectTo(".//google.com"));
+ assertFalse(Util.isSafeToRedirectTo(".///google.com"));
+ assertFalse(Util.isSafeToRedirectTo("aaa/..//google.com"));
+ assertFalse(Util.isSafeToRedirectTo("./aaa/..//google.com"));
+ assertFalse(Util.isSafeToRedirectTo("./\\/google.com"));
+
+ assertFalse(Util.isSafeToRedirectTo("/\t/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\n/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\r/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("\t//google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\t/\t/google.com"));
+ assertFalse(Util.isSafeToRedirectTo("/\t\n\r/google.com"));
+
assertTrue(Util.isSafeToRedirectTo("foo/bar/abc:def"));
assertTrue(Util.isSafeToRedirectTo("foo?abc:def"));
assertTrue(Util.isSafeToRedirectTo("foo#abc:def"));
test/src/test/java/jenkins/security/Security3501Test.java+15 6
@@ -13,6 +13,7 @@
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.ValueSource;
+import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.junit.jupiter.RealJenkinsExtension;
@@ -31,6 +32,7 @@ void setUp() {
jj.withPrefix(contextPath);
}
+ @Issue({"SECURITY-3501", "SECURITY-3711"})
@Test
void testRedirects() throws Throwable {
jj.then(new TestRedirectsStep(contextPath));
@@ -38,20 +40,27 @@ void testRedirects() throws Throwable {
private record TestRedirectsStep(String context) implements RealJenkinsExtension.Step {
public void run(JenkinsRule j) throws Exception {
- List<String> prohibitedPaths = List.of("%5C%5Cexample.org", "%5C/example.org", "/%5Cexample.org", "//example.org", "https://example.org", "\\example.org");
+ List<String> prohibitedPaths = List.of(
+ // SECURITY-3501
+ "%5C%5Cexample.org", "%5C/example.org", "/%5Cexample.org", "//example.org",
+ "https://example.org", "\\example.org",
+ ".//example.org", ".///example.org", ".////example.org",
+ "aaa/..//example.org", "./aaa/..//example.org", "./%5C/example.org",
+ "/%09/example.org", "/%0A/example.org", "/%0D/example.org",
+ "%09//example.org", "/%09/%09/example.org");
for (String path : prohibitedPaths) {
try (JenkinsRule.WebClient wc = j.createWebClient().withRedirectEnabled(false)) {
- final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path));
- assertThat(fhsce.getStatusCode(), is(404));
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path), "path=" + path);
+ assertThat("path=" + path, fhsce.getStatusCode(), is(404));
}
}
List<String> allowedPaths = List.of("foo", "foo/bar");
for (String path : allowedPaths) {
try (JenkinsRule.WebClient wc = j.createWebClient().withRedirectEnabled(false)) {
- final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path));
- assertThat(fhsce.getStatusCode(), is(302));
- assertThat(fhsce.getResponse().getResponseHeaderValue("Location"), is(context + "/redirects/" + path));
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("redirects/content?path=" + path), "path=" + path);
+ assertThat("path=" + path, fhsce.getStatusCode(), is(302));
+ assertThat("path=" + path, fhsce.getResponse().getResponseHeaderValue("Location"), is(context + "/redirects/" + path));
}
}
}

References