Jenkins: Open Redirect phishing attacks possible via "from" parameter in "Delegate to servlet container"
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]
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]
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
- ADVISORYhttps://nvd.nist.gov/vuln/detail/CVE-2026-53440
- WEBhttps://github.com/jenkinsci/jenkins/commit/38071826c9a2113e1104714595262827a87b392f
- WEBhttps://github.com/jenkinsci/jenkins/commit/c45e93f2d77d94ea3b0545eb5aca32b808a27586
- PACKAGEhttps://github.com/jenkinsci/jenkins
- WEBhttps://www.jenkins.io/security/advisory/2026-06-10/#SECURITY-3721