Jenkins Open Redirect Through Newline/Tab Characters in Redirect URL
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
0 → fixed in 2.555.32.556 → fixed in 2.568
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 tab or newline characters between `//`, allowing attackers to perform phishing attacks.
The fix
[SECURITY-3711][SECURITY-3755]
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"})@Testvoid 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]
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"})@Testvoid 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
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2026-53437
- WEBhttps://github.com/jenkinsci/jenkins/commit/b32f2f27a82ed187a34f55b05edcc4a83563d574
- WEBhttps://github.com/jenkinsci/jenkins/commit/8ef52891b07eb639b38271e4bab5dab3c0f10fda
- WEBhttps://www.jenkins.io/security/advisory/2026-06-10/#SECURITY-3711+3755
- WEBhttps://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-53437.json
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://bugzilla.redhat.com/show_bug.cgi?id=2487544
- WEBhttps://access.redhat.com/security/cve/CVE-2026-53437
- WEBhttps://access.redhat.com/errata/RHSA-2026:60259
- WEBhttps://access.redhat.com/errata/RHSA-2026:60256
- WEBhttps://access.redhat.com/errata/RHSA-2026:60254
- WEBhttps://access.redhat.com/errata/RHSA-2026:60252