Security context
Low· 3.5GHSA-6837-qgrc-x5p6 CVE-2025-67639CWE-352Published Dec 10, 2025

Jenkins has a CSRF vulnerability on the login form

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.529 → fixed in 2.5410 → fixed in 2.528.3

Details

A cross-site request forgery (CSRF) vulnerability in Jenkins 2.540 and earlier, LTS 2.528.2 and earlier allows attackers to trick users into logging in to the attacker's account.

The fix

[SECURITY-1166]

Kevin-CB· Dec 2, 2025, 01:46 PM+245031598feb0a
core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java+34 0
@@ -26,6 +26,7 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.model.User;
+import hudson.security.csrf.CrumbIssuer;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -34,12 +35,14 @@
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
+import jenkins.model.Jenkins;
import jenkins.security.SecurityListener;
import jenkins.security.seed.UserSeedProperty;
import jenkins.util.SystemProperties;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -55,6 +58,13 @@
@Restricted(NoExternalUse.class)
public final class AuthenticationProcessingFilter2 extends UsernamePasswordAuthenticationFilter {
+ /**
+ * Escape hatch to disable CSRF protection for login requests.
+ */
+ @Restricted(NoExternalUse.class)
+ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")
+ public static /* non-final */ boolean SKIP_CSRF_CHECK = SystemProperties.getBoolean(AuthenticationProcessingFilter2.class.getName() + ".skipCSRFCheck");
+
@SuppressFBWarnings(value = "HARD_CODE_PASSWORD", justification = "This is a password parameter, not a password")
public AuthenticationProcessingFilter2(String authenticationGatewayUrl) {
setRequiresAuthenticationRequestMatcher(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, "/" + authenticationGatewayUrl));
@@ -63,6 +73,30 @@ public AuthenticationProcessingFilter2(String authenticationGatewayUrl) {
setPasswordParameter("j_password");
}
+ @Override
+ public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
+ if (!SKIP_CSRF_CHECK) {
+ Jenkins jenkins = Jenkins.get();
+ CrumbIssuer crumbIssuer = jenkins.getCrumbIssuer();
+ if (crumbIssuer != null) {
+ String crumbField = crumbIssuer.getCrumbRequestField();
+ String crumbSalt = crumbIssuer.getDescriptor().getCrumbSalt();
+
+ String crumb = request.getParameter(crumbField);
+ if (crumb == null) {
+ crumb = request.getHeader(crumbField);
+ }
+
+ if (crumb == null || !crumbIssuer.validateCrumb(request, crumbSalt, crumb)) {
+ LOGGER.log(Level.FINE, "No valid crumb was included in authentication request from {0}", request.getRemoteAddr());
+ throw new InsufficientAuthenticationException("No valid crumb was included in the request");
+ }
+ }
+ }
+
+ return super.attemptAuthentication(request, response);
+ }
+
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (SystemProperties.getInteger(SecurityRealm.class.getName() + ".sessionFixationProtectionMode", 1) == 2) {
core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly+1 0
@@ -6,6 +6,7 @@
<st:include page="client-scripts" />
<form action="${app.instance.securityRealm.authenticationGatewayUrl}" method="POST">
<input type="hidden" name="from" value="${request2.getParameter('from')}" />
+ <input type="hidden" name="${h.getCrumbRequestField()}" value="${h.getCrumb(request2)}"/>
<div class="plugin-setup-wizard bootstrap-3">
<div class="modal fade in" style="display: block;">
<div class="modal-dialog">
core/src/main/resources/jenkins/model/Jenkins/login.jelly+1 0
@@ -116,6 +116,7 @@ THE SOFTWARE.
</div>
</j:if>
<input type="hidden" name="from" value="${from}"/>
+ <input type="hidden" name="${h.getCrumbRequestField()}" value="${h.getCrumb(request2)}"/>
<button type="submit"
name="Submit"
class="jenkins-button jenkins-button--primary">
test/src/test/java/jenkins/security/Security1166Test.java+209 0
@@ -0,0 +1,209 @@
+package jenkins.security;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasProperty;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
+import static org.hamcrest.xml.HasXPath.hasXPath;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import org.htmlunit.HttpMethod;
+import org.htmlunit.WebRequest;
+import org.htmlunit.WebResponse;
+import org.htmlunit.html.HtmlForm;
+import org.htmlunit.html.HtmlFormUtil;
+import org.htmlunit.html.HtmlPage;
+import org.htmlunit.util.NameValuePair;
+import org.htmlunit.xml.XmlPage;
+import org.junit.jupiter.api.BeforeEach;
+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-1166")
+@WithJenkins
+class Security1166Test {
+
+ private JenkinsRule j;
+
+ @BeforeEach
+ void setUp(JenkinsRule rule) {
+ j = rule;
+ }
+
+ @Test
+ void loginRequestFailWithNoCrumb() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ wc.goTo(""); // to trigger the creation of a session
+
+ WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
+ List<NameValuePair> params = new ArrayList<>();
+ params.add(new NameValuePair("j_username", "alice"));
+ params.add(new NameValuePair("j_password", "alice"));
+
+ request.setRequestParameters(params);
+
+ WebResponse response = wc.getPage(request).getWebResponse();
+ assertEquals(401, response.getStatusCode());
+ assertUserNotConnected(wc, "alice");
+ }
+ }
+
+ @Test
+ void loginRequestFailWithInvalidCrumb() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ wc.goTo(""); // to trigger the creation of a session
+
+ WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
+ List<NameValuePair> params = new ArrayList<>();
+ params.add(new NameValuePair("j_username", "alice"));
+ params.add(new NameValuePair("j_password", "alice"));
+
+ String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
+ params.add(new NameValuePair(crumbField, "invalid-crumb"));
+
+ request.setRequestParameters(params);
+
+ WebResponse response = wc.getPage(request).getWebResponse();
+ assertEquals(401, response.getStatusCode());
+ assertUserNotConnected(wc, "alice");
+ }
+ }
+
+ @Test
+ void loginRequestSucceedWithValidCrumb() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ wc.goTo(""); // to trigger the creation of a session
+
+ WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
+ List<NameValuePair> params = new ArrayList<>();
+ params.add(new NameValuePair("j_username", "alice"));
+ params.add(new NameValuePair("j_password", "alice"));
+
+ String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
+ String crumbValue = j.jenkins.getCrumbIssuer().getCrumb();
+ params.add(new NameValuePair(crumbField, crumbValue));
+
+ request.setRequestParameters(params);
+
+ WebResponse response = wc.getPage(request).getWebResponse();
+ assertEquals(200, response.getStatusCode());
+ assertUserConnected(wc, "alice");
+ }
+ }
+
+ @Test
+ void loginRequestSucceedWithCrumbInHeader() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ wc.goTo(""); // to trigger the creation of a session
+
+ WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
+ List<NameValuePair> params = new ArrayList<>();
+ params.add(new NameValuePair("j_username", "alice"));
+ params.add(new NameValuePair("j_password", "alice"));
+
+ String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
+ String crumbValue = j.jenkins.getCrumbIssuer().getCrumb();
+ request.setAdditionalHeader(crumbField, crumbValue);
+
+ request.setRequestParameters(params);
+
+ WebResponse response = wc.getPage(request).getWebResponse();
+ assertEquals(200, response.getStatusCode());
+ assertUserConnected(wc, "alice");
+ }
+ }
+
+ @Test
+ void loginRequestSucceedWithNoCrumbIssuer() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ j.jenkins.setCrumbIssuer(null);
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ wc.goTo(""); // to trigger the creation of a session
+
+ WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
+ List<NameValuePair> params = new ArrayList<>();
+ params.add(new NameValuePair("j_username", "alice"));
+ params.add(new NameValuePair("j_password", "alice"));
+
+ request.setRequestParameters(params);
+
+ WebResponse response = wc.getPage(request).getWebResponse();
+ assertEquals(200, response.getStatusCode());
+ assertUserConnected(wc, "alice");
+ }
+ }
+
+ @Test
+ void loginRequestFailWithGET() throws Exception {
+ j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ wc.goTo(""); // to trigger the creation of a session
+ wc.setRedirectEnabled(false); // disabling redirection to demonstrates that Spring did not handle the request
+
+ WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.GET);
+ List<NameValuePair> params = new ArrayList<>();
+ params.add(new NameValuePair("j_username", "alice"));
+ params.add(new NameValuePair("j_password", "alice"));
+
+ String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
+ String crumbValue = j.jenkins.getCrumbIssuer().getCrumb();
+ params.add(new NameValuePair(crumbField, crumbValue));
+
+ request.setRequestParameters(params);
+
+ WebResponse response = wc.getPage(request).getWebResponse();
+ assertEquals(404, response.getStatusCode());
+ assertThat(response.getResponseHeaders(), hasItem(hasProperty("name", startsWith("Stapler-Trace"))));
+ assertUserNotConnected(wc, "alice");
+ }
+ }
+
+ @Test
+ void loginThroughSetupWizard() throws Exception {
+ j.jenkins.setInstallState(jenkins.install.InstallState.INITIAL_SECURITY_SETUP);
+
+ String initialAdminPassword = j.jenkins.getSetupWizard().getInitialAdminPasswordFile().readToString().trim();
+
+ try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
+ HtmlPage page = wc.goTo("login");
+ List<HtmlForm> forms = page.getForms();
+ HtmlForm form = forms.get(0);
+
+ assertEquals(1, forms.size()); // It's the only form, which doesn't have a name or an id.
+
+ form.getInputByName("j_password").setValue(initialAdminPassword);
+
+ HtmlFormUtil.submit(form, null);
+
+ assertUserConnected(wc, "admin");
+ }
+ }
+
+ private void assertUserConnected(JenkinsRule.WebClient wc, String expectedUsername) throws Exception {
+ XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
+ assertThat(page, hasXPath("//name", is(expectedUsername)));
+ }
+
+ private void assertUserNotConnected(JenkinsRule.WebClient wc, String notExpectedUsername) throws Exception {
+ XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
+ assertThat(page, hasXPath("//name", not(is(notExpectedUsername))));
+ }
+}

References