Security context
Medium· 4.3GHSA-92m7-4fpw-2wxm CVE-2026-53440CWE-601Published Jun 10, 2026

Jenkins: Open Redirect phishing attacks possible via "from" parameter in "Delegate to servlet container"

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

0 → fixed in 2.555.32.556 → fixed in 2.568

Details

Jenkins 2.567 and earlier, LTS 2.555.2 and earlier does not ensure that the "from" parameter in the "Delegate to servlet container" security realm is safe to redirect to after login, allowing attackers to perform phishing attacks by redirecting users to an attacker-controlled domain.

The fix

[SECURITY-3721]

Kevin-CB· May 29, 2026, 07:39 AM+49238071826c9
core/src/main/java/jenkins/model/Jenkins.java+2 2
@@ -4340,8 +4340,8 @@ public void doLoginEntry(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExc
// TODO fire something in SecurityListener?
String from = req.getParameter("from");
- if (from != null && from.startsWith("/") && !from.equals("/loginError")) {
- rsp.sendRedirect2(from); // I'm bit uncomfortable letting users redirected to other sites, make sure the URL falls into this domain
+ if (from != null && Util.isSafeToRedirectTo(from)) {
+ rsp.sendRedirect2(from);
return;
}
test/src/test/java/jenkins/model/Security3721Test.java+47 0
@@ -0,0 +1,47 @@
+package jenkins.model;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import hudson.security.LegacySecurityRealm;
+import java.util.List;
+import org.htmlunit.FailingHttpStatusCodeException;
+import org.junit.jupiter.api.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+@Issue("SECURITY-3721")
+@WithJenkins
+public class Security3721Test {
+
+ @Test
+ void openRedirectIsBlocked(JenkinsRule j) throws Exception {
+ j.jenkins.setSecurityRealm(new LegacySecurityRealm());
+
+ List<String> prohibitedPaths = List.of("%5C%5Cexample.org", "%5C/example.org", "/%5Cexample.org", "//example.org", "https://example.org", "\\example.org");
+ for (String path : prohibitedPaths) {
+ try (JenkinsRule.WebClient wc = j.createWebClient()) {
+ wc.login("alice");
+ wc.setRedirectEnabled(false);
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("loginEntry?from=" + path));
+
+ assertThat(fhsce.getStatusCode(), is(302));
+ assertThat(fhsce.getResponse().getResponseHeaderValue("Location"), is("/jenkins/"));
+ }
+ }
+
+ List<String> allowedPaths = List.of("configure", "manage/security");
+ for (String path : allowedPaths) {
+ try (JenkinsRule.WebClient wc = j.createWebClient()) {
+ wc.login("alice");
+ wc.setRedirectEnabled(false);
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("loginEntry?from=" + path));
+
+ assertThat(fhsce.getStatusCode(), is(302));
+ assertThat(fhsce.getResponse().getResponseHeaderValue("Location"), is("/jenkins/" + path));
+ }
+ }
+ }
+}

[SECURITY-3721]

Kevin-CB· Jun 2, 2026, 10:14 AM+492c45e93f2d7
core/src/main/java/jenkins/model/Jenkins.java+2 2
@@ -4340,8 +4340,8 @@ public void doLoginEntry(StaplerRequest2 req, StaplerResponse2 rsp) throws IOExc
// TODO fire something in SecurityListener?
String from = req.getParameter("from");
- if (from != null && from.startsWith("/") && !from.equals("/loginError")) {
- rsp.sendRedirect2(from); // I'm bit uncomfortable letting users redirected to other sites, make sure the URL falls into this domain
+ if (from != null && Util.isSafeToRedirectTo(from)) {
+ rsp.sendRedirect2(from);
return;
}
test/src/test/java/jenkins/model/Security3721Test.java+47 0
@@ -0,0 +1,47 @@
+package jenkins.model;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import hudson.security.LegacySecurityRealm;
+import java.util.List;
+import org.htmlunit.FailingHttpStatusCodeException;
+import org.junit.jupiter.api.Test;
+import org.jvnet.hudson.test.Issue;
+import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
+
+@Issue("SECURITY-3721")
+@WithJenkins
+public class Security3721Test {
+
+ @Test
+ void openRedirectIsBlocked(JenkinsRule j) throws Exception {
+ j.jenkins.setSecurityRealm(new LegacySecurityRealm());
+
+ List<String> prohibitedPaths = List.of("%5C%5Cexample.org", "%5C/example.org", "/%5Cexample.org", "//example.org", "https://example.org", "\\example.org");
+ for (String path : prohibitedPaths) {
+ try (JenkinsRule.WebClient wc = j.createWebClient()) {
+ wc.login("alice");
+ wc.setRedirectEnabled(false);
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("loginEntry?from=" + path));
+
+ assertThat(fhsce.getStatusCode(), is(302));
+ assertThat(fhsce.getResponse().getResponseHeaderValue("Location"), is("/jenkins/"));
+ }
+ }
+
+ List<String> allowedPaths = List.of("configure", "manage/security");
+ for (String path : allowedPaths) {
+ try (JenkinsRule.WebClient wc = j.createWebClient()) {
+ wc.login("alice");
+ wc.setRedirectEnabled(false);
+ final FailingHttpStatusCodeException fhsce = assertThrows(FailingHttpStatusCodeException.class, () -> wc.goTo("loginEntry?from=" + path));
+
+ assertThat(fhsce.getStatusCode(), is(302));
+ assertThat(fhsce.getResponse().getResponseHeaderValue("Location"), is("/jenkins/" + path));
+ }
+ }
+ }
+}

References