Skip to content

Commit 20bc304

Browse files
Kevin-CBjenkinsci-cert-ci
authored andcommitted
1 parent 042de66 commit 20bc304

File tree

4 files changed

+245
-0
lines changed

4 files changed

+245
-0
lines changed

core/src/main/java/hudson/security/AuthenticationProcessingFilter2.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
2828
import hudson.model.User;
29+
import hudson.security.csrf.CrumbIssuer;
2930
import jakarta.servlet.FilterChain;
3031
import jakarta.servlet.ServletException;
3132
import jakarta.servlet.http.HttpServletRequest;
@@ -34,12 +35,14 @@
3435
import java.io.IOException;
3536
import java.util.logging.Level;
3637
import java.util.logging.Logger;
38+
import jenkins.model.Jenkins;
3739
import jenkins.security.SecurityListener;
3840
import jenkins.security.seed.UserSeedProperty;
3941
import jenkins.util.SystemProperties;
4042
import org.kohsuke.accmod.Restricted;
4143
import org.kohsuke.accmod.restrictions.NoExternalUse;
4244
import org.springframework.http.HttpMethod;
45+
import org.springframework.security.authentication.InsufficientAuthenticationException;
4346
import org.springframework.security.core.Authentication;
4447
import org.springframework.security.core.AuthenticationException;
4548
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -55,6 +58,13 @@
5558
@Restricted(NoExternalUse.class)
5659
public final class AuthenticationProcessingFilter2 extends UsernamePasswordAuthenticationFilter {
5760

61+
/**
62+
* Escape hatch to disable CSRF protection for login requests.
63+
*/
64+
@Restricted(NoExternalUse.class)
65+
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "for script console")
66+
public static /* non-final */ boolean SKIP_CSRF_CHECK = SystemProperties.getBoolean(AuthenticationProcessingFilter2.class.getName() + ".skipCSRFCheck");
67+
5868
@SuppressFBWarnings(value = "HARD_CODE_PASSWORD", justification = "This is a password parameter, not a password")
5969
public AuthenticationProcessingFilter2(String authenticationGatewayUrl) {
6070
setRequiresAuthenticationRequestMatcher(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, "/" + authenticationGatewayUrl));
@@ -63,6 +73,30 @@ public AuthenticationProcessingFilter2(String authenticationGatewayUrl) {
6373
setPasswordParameter("j_password");
6474
}
6575

76+
@Override
77+
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
78+
if (!SKIP_CSRF_CHECK) {
79+
Jenkins jenkins = Jenkins.get();
80+
CrumbIssuer crumbIssuer = jenkins.getCrumbIssuer();
81+
if (crumbIssuer != null) {
82+
String crumbField = crumbIssuer.getCrumbRequestField();
83+
String crumbSalt = crumbIssuer.getDescriptor().getCrumbSalt();
84+
85+
String crumb = request.getParameter(crumbField);
86+
if (crumb == null) {
87+
crumb = request.getHeader(crumbField);
88+
}
89+
90+
if (crumb == null || !crumbIssuer.validateCrumb(request, crumbSalt, crumb)) {
91+
LOGGER.log(Level.FINE, "No valid crumb was included in authentication request from {0}", request.getRemoteAddr());
92+
throw new InsufficientAuthenticationException("No valid crumb was included in the request");
93+
}
94+
}
95+
}
96+
97+
return super.attemptAuthentication(request, response);
98+
}
99+
66100
@Override
67101
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
68102
if (SystemProperties.getInteger(SecurityRealm.class.getName() + ".sessionFixationProtectionMode", 1) == 2) {

core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<st:include page="client-scripts" />
77
<form action="${app.instance.securityRealm.authenticationGatewayUrl}" method="POST">
88
<input type="hidden" name="from" value="${request2.getParameter('from')}" />
9+
<input type="hidden" name="${h.getCrumbRequestField()}" value="${h.getCrumb(request2)}"/>
910
<div class="plugin-setup-wizard bootstrap-3">
1011
<div class="modal fade in" style="display: block;">
1112
<div class="modal-dialog">

core/src/main/resources/jenkins/model/Jenkins/login.jelly

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ THE SOFTWARE.
116116
</div>
117117
</j:if>
118118
<input type="hidden" name="from" value="${from}"/>
119+
<input type="hidden" name="${h.getCrumbRequestField()}" value="${h.getCrumb(request2)}"/>
119120
<button type="submit"
120121
name="Submit"
121122
class="jenkins-button jenkins-button--primary">
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package jenkins.security;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.hasItem;
5+
import static org.hamcrest.Matchers.hasProperty;
6+
import static org.hamcrest.Matchers.is;
7+
import static org.hamcrest.Matchers.not;
8+
import static org.hamcrest.Matchers.startsWith;
9+
import static org.hamcrest.xml.HasXPath.hasXPath;
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
12+
import java.net.URL;
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import org.htmlunit.HttpMethod;
16+
import org.htmlunit.WebRequest;
17+
import org.htmlunit.WebResponse;
18+
import org.htmlunit.html.HtmlForm;
19+
import org.htmlunit.html.HtmlFormUtil;
20+
import org.htmlunit.html.HtmlPage;
21+
import org.htmlunit.util.NameValuePair;
22+
import org.htmlunit.xml.XmlPage;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.jvnet.hudson.test.Issue;
26+
import org.jvnet.hudson.test.JenkinsRule;
27+
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;
28+
29+
@Issue("SECURITY-1166")
30+
@WithJenkins
31+
class Security1166Test {
32+
33+
private JenkinsRule j;
34+
35+
@BeforeEach
36+
void setUp(JenkinsRule rule) {
37+
j = rule;
38+
}
39+
40+
@Test
41+
void loginRequestFailWithNoCrumb() throws Exception {
42+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
43+
44+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
45+
wc.goTo(""); // to trigger the creation of a session
46+
47+
WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
48+
List<NameValuePair> params = new ArrayList<>();
49+
params.add(new NameValuePair("j_username", "alice"));
50+
params.add(new NameValuePair("j_password", "alice"));
51+
52+
request.setRequestParameters(params);
53+
54+
WebResponse response = wc.getPage(request).getWebResponse();
55+
assertEquals(401, response.getStatusCode());
56+
assertUserNotConnected(wc, "alice");
57+
}
58+
}
59+
60+
@Test
61+
void loginRequestFailWithInvalidCrumb() throws Exception {
62+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
63+
64+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
65+
wc.goTo(""); // to trigger the creation of a session
66+
67+
WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
68+
List<NameValuePair> params = new ArrayList<>();
69+
params.add(new NameValuePair("j_username", "alice"));
70+
params.add(new NameValuePair("j_password", "alice"));
71+
72+
String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
73+
params.add(new NameValuePair(crumbField, "invalid-crumb"));
74+
75+
request.setRequestParameters(params);
76+
77+
WebResponse response = wc.getPage(request).getWebResponse();
78+
assertEquals(401, response.getStatusCode());
79+
assertUserNotConnected(wc, "alice");
80+
}
81+
}
82+
83+
@Test
84+
void loginRequestSucceedWithValidCrumb() throws Exception {
85+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
86+
87+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
88+
wc.goTo(""); // to trigger the creation of a session
89+
90+
WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
91+
List<NameValuePair> params = new ArrayList<>();
92+
params.add(new NameValuePair("j_username", "alice"));
93+
params.add(new NameValuePair("j_password", "alice"));
94+
95+
String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
96+
String crumbValue = j.jenkins.getCrumbIssuer().getCrumb();
97+
params.add(new NameValuePair(crumbField, crumbValue));
98+
99+
request.setRequestParameters(params);
100+
101+
WebResponse response = wc.getPage(request).getWebResponse();
102+
assertEquals(200, response.getStatusCode());
103+
assertUserConnected(wc, "alice");
104+
}
105+
}
106+
107+
@Test
108+
void loginRequestSucceedWithCrumbInHeader() throws Exception {
109+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
110+
111+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
112+
wc.goTo(""); // to trigger the creation of a session
113+
114+
WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
115+
List<NameValuePair> params = new ArrayList<>();
116+
params.add(new NameValuePair("j_username", "alice"));
117+
params.add(new NameValuePair("j_password", "alice"));
118+
119+
String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
120+
String crumbValue = j.jenkins.getCrumbIssuer().getCrumb();
121+
request.setAdditionalHeader(crumbField, crumbValue);
122+
123+
request.setRequestParameters(params);
124+
125+
WebResponse response = wc.getPage(request).getWebResponse();
126+
assertEquals(200, response.getStatusCode());
127+
assertUserConnected(wc, "alice");
128+
}
129+
}
130+
131+
@Test
132+
void loginRequestSucceedWithNoCrumbIssuer() throws Exception {
133+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
134+
135+
j.jenkins.setCrumbIssuer(null);
136+
137+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
138+
wc.goTo(""); // to trigger the creation of a session
139+
140+
WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.POST);
141+
List<NameValuePair> params = new ArrayList<>();
142+
params.add(new NameValuePair("j_username", "alice"));
143+
params.add(new NameValuePair("j_password", "alice"));
144+
145+
request.setRequestParameters(params);
146+
147+
WebResponse response = wc.getPage(request).getWebResponse();
148+
assertEquals(200, response.getStatusCode());
149+
assertUserConnected(wc, "alice");
150+
}
151+
}
152+
153+
@Test
154+
void loginRequestFailWithGET() throws Exception {
155+
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
156+
157+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
158+
wc.goTo(""); // to trigger the creation of a session
159+
wc.setRedirectEnabled(false); // disabling redirection to demonstrates that Spring did not handle the request
160+
161+
WebRequest request = new WebRequest(new URL(j.getURL(), "j_spring_security_check"), HttpMethod.GET);
162+
List<NameValuePair> params = new ArrayList<>();
163+
params.add(new NameValuePair("j_username", "alice"));
164+
params.add(new NameValuePair("j_password", "alice"));
165+
166+
String crumbField = j.jenkins.getCrumbIssuer().getCrumbRequestField();
167+
String crumbValue = j.jenkins.getCrumbIssuer().getCrumb();
168+
params.add(new NameValuePair(crumbField, crumbValue));
169+
170+
request.setRequestParameters(params);
171+
172+
WebResponse response = wc.getPage(request).getWebResponse();
173+
assertEquals(404, response.getStatusCode());
174+
assertThat(response.getResponseHeaders(), hasItem(hasProperty("name", startsWith("Stapler-Trace"))));
175+
assertUserNotConnected(wc, "alice");
176+
}
177+
}
178+
179+
@Test
180+
void loginThroughSetupWizard() throws Exception {
181+
j.jenkins.setInstallState(jenkins.install.InstallState.INITIAL_SECURITY_SETUP);
182+
183+
String initialAdminPassword = j.jenkins.getSetupWizard().getInitialAdminPasswordFile().readToString().trim();
184+
185+
try (JenkinsRule.WebClient wc = j.createWebClient().withThrowExceptionOnFailingStatusCode(false)) {
186+
HtmlPage page = wc.goTo("login");
187+
List<HtmlForm> forms = page.getForms();
188+
HtmlForm form = forms.get(0);
189+
190+
assertEquals(1, forms.size()); // It's the only form, which doesn't have a name or an id.
191+
192+
form.getInputByName("j_password").setValue(initialAdminPassword);
193+
194+
HtmlFormUtil.submit(form, null);
195+
196+
assertUserConnected(wc, "admin");
197+
}
198+
}
199+
200+
private void assertUserConnected(JenkinsRule.WebClient wc, String expectedUsername) throws Exception {
201+
XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
202+
assertThat(page, hasXPath("//name", is(expectedUsername)));
203+
}
204+
205+
private void assertUserNotConnected(JenkinsRule.WebClient wc, String notExpectedUsername) throws Exception {
206+
XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
207+
assertThat(page, hasXPath("//name", not(is(notExpectedUsername))));
208+
}
209+
}

0 commit comments

Comments
 (0)