mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-15 04:52:20 +00:00
Merge pull request #3974 from jsoref/SECURITY-followup
Security followup
This commit is contained in:
12 files changed
+528
-738
No files matched your search
@@ -1,35 +0,0 @@
|
||||
package hudson;
|
||||
|
||||
import hudson.model.UpdateCenter;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import org.junit.Ignore;
|
||||
|
||||
//TODO merge it within PluginTest after the security release
|
||||
public class PluginSEC925Test {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule r = new JenkinsRule();
|
||||
|
||||
@Ignore("TODO observed to fail in CI with 404 due to external UC issues")
|
||||
@Test
|
||||
@Issue("SECURITY-925")
|
||||
public void preventTimestamp2_toBeServed() throws Exception {
|
||||
// impossible to use installDetachedPlugin("credentials") since we want to have it exploded like with WAR
|
||||
Jenkins.getInstance().getUpdateCenter().getSites().get(0).updateDirectlyNow(false);
|
||||
List<Future<UpdateCenter.UpdateCenterJob>> pluginInstalled = r.jenkins.pluginManager.install(Arrays.asList("credentials"), true);
|
||||
|
||||
for (Future<UpdateCenter.UpdateCenterJob> job : pluginInstalled) {
|
||||
job.get();
|
||||
}
|
||||
r.createWebClient().assertFails("plugin/credentials/.timestamp2", HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,14 @@
|
||||
|
||||
package hudson;
|
||||
|
||||
import hudson.model.UpdateCenter;
|
||||
import jenkins.model.Jenkins;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
import org.junit.Ignore;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.junit.Test;
|
||||
import org.junit.Rule;
|
||||
@@ -57,4 +65,17 @@ public class PluginTest {
|
||||
r.createWebClient().assertFails("plugin/credentials/meta-inf/manifest.mf", HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Ignore("TODO observed to fail in CI with 404 due to external UC issues")
|
||||
@Test
|
||||
@Issue("SECURITY-925")
|
||||
public void preventTimestamp2_toBeServed() throws Exception {
|
||||
// impossible to use installDetachedPlugin("credentials") since we want to have it exploded like with WAR
|
||||
Jenkins.getInstance().getUpdateCenter().getSites().get(0).updateDirectlyNow(false);
|
||||
List<Future<UpdateCenter.UpdateCenterJob>> pluginInstalled = r.jenkins.pluginManager.install(Arrays.asList("credentials"), true);
|
||||
|
||||
for (Future<UpdateCenter.UpdateCenterJob> job : pluginInstalled) {
|
||||
job.get();
|
||||
}
|
||||
r.createWebClient().assertFails("plugin/credentials/.timestamp2", HttpServletResponse.SC_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package hudson.model;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import hudson.model.Cause.UserIdCause;
|
||||
import hudson.slaves.NodeProvisionerRule;
|
||||
import jenkins.model.Jenkins;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.MockAuthorizationStrategy;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
//TODO merge into QueueTest after security patch
|
||||
public class QueueSEC891Test {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule r = new NodeProvisionerRule(-1, 0, 10);
|
||||
|
||||
@Test public void doCancelItem_PermissionIsChecked() throws Exception {
|
||||
checkCancelOperationUsingUrl(item -> "queue/cancelItem?id=" + item.getId());
|
||||
}
|
||||
|
||||
@Test public void doCancelQueue_PermissionIsChecked() throws Exception {
|
||||
checkCancelOperationUsingUrl(item -> "queue/item/" + item.getId() + "/cancelQueue");
|
||||
}
|
||||
|
||||
private void checkCancelOperationUsingUrl(Function<Queue.Item, String> urlProvider) throws Exception {
|
||||
Queue q = r.jenkins.getQueue();
|
||||
|
||||
r.jenkins.setCrumbIssuer(null);
|
||||
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
|
||||
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
|
||||
.grant(Jenkins.READ, Item.CANCEL).everywhere().to("admin")
|
||||
.grant(Jenkins.READ).everywhere().to("user")
|
||||
);
|
||||
|
||||
// prevent execution to push stuff into the queue
|
||||
r.jenkins.setNumExecutors(0);
|
||||
assertThat(q.getItems().length, equalTo(0));
|
||||
|
||||
FreeStyleProject testProject = r.createFreeStyleProject("test");
|
||||
testProject.scheduleBuild(new UserIdCause());
|
||||
|
||||
Queue.Item[] items = q.getItems();
|
||||
assertThat(items.length, equalTo(1));
|
||||
Queue.Item currentOne = items[0];
|
||||
assertFalse(currentOne.getFuture().isCancelled());
|
||||
|
||||
WebRequest request = new WebRequest(new URL(r.getURL() + urlProvider.apply(currentOne)), HttpMethod.POST);
|
||||
|
||||
{ // user without right cannot cancel
|
||||
JenkinsRule.WebClient wc = r.createWebClient()
|
||||
.withRedirectEnabled(false)
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
wc.login("user");
|
||||
Page p = wc.getPage(request);
|
||||
// currently the endpoint return a redirection to the previously visited page, none in our case
|
||||
// (so force no redirect to avoid false positive error)
|
||||
assertThat(p.getWebResponse().getStatusCode(), lessThan(400));
|
||||
|
||||
assertFalse(currentOne.getFuture().isCancelled());
|
||||
}
|
||||
{ // user with right can
|
||||
JenkinsRule.WebClient wc = r.createWebClient()
|
||||
.withRedirectEnabled(false)
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
wc.login("admin");
|
||||
Page p = wc.getPage(request);
|
||||
assertThat(p.getWebResponse().getStatusCode(), lessThan(400));
|
||||
|
||||
assertTrue(currentOne.getFuture().isCancelled());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@
|
||||
*/
|
||||
package hudson.model;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import com.gargoylesoftware.htmlunit.html.DomElement;
|
||||
import com.gargoylesoftware.htmlunit.html.DomNode;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlFileInput;
|
||||
@@ -93,6 +96,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.MockAuthorizationStrategy;
|
||||
import org.jvnet.hudson.test.MockQueueItemAuthenticator;
|
||||
import org.jvnet.hudson.test.SequenceLock;
|
||||
import org.jvnet.hudson.test.SleepBuilder;
|
||||
@@ -107,6 +111,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -121,9 +126,12 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Ignore;
|
||||
@@ -1067,4 +1075,64 @@ public class QueueTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-891")
|
||||
public void doCancelItem_PermissionIsChecked() throws Exception {
|
||||
checkCancelOperationUsingUrl(item -> "queue/cancelItem?id=" + item.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-891")
|
||||
public void doCancelQueue_PermissionIsChecked() throws Exception {
|
||||
checkCancelOperationUsingUrl(item -> "queue/item/" + item.getId() + "/cancelQueue");
|
||||
}
|
||||
|
||||
private void checkCancelOperationUsingUrl(Function<Queue.Item, String> urlProvider) throws Exception {
|
||||
Queue q = r.jenkins.getQueue();
|
||||
|
||||
r.jenkins.setCrumbIssuer(null);
|
||||
r.jenkins.setSecurityRealm(r.createDummySecurityRealm());
|
||||
r.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
|
||||
.grant(Jenkins.READ, Item.CANCEL).everywhere().to("admin")
|
||||
.grant(Jenkins.READ).everywhere().to("user")
|
||||
);
|
||||
|
||||
// prevent execution to push stuff into the queue
|
||||
r.jenkins.setNumExecutors(0);
|
||||
assertThat(q.getItems().length, equalTo(0));
|
||||
|
||||
FreeStyleProject testProject = r.createFreeStyleProject("test");
|
||||
testProject.scheduleBuild(new UserIdCause());
|
||||
|
||||
Queue.Item[] items = q.getItems();
|
||||
assertThat(items.length, equalTo(1));
|
||||
Queue.Item currentOne = items[0];
|
||||
assertFalse(currentOne.getFuture().isCancelled());
|
||||
|
||||
WebRequest request = new WebRequest(new URL(r.getURL() + urlProvider.apply(currentOne)), HttpMethod.POST);
|
||||
|
||||
{ // user without right cannot cancel
|
||||
JenkinsRule.WebClient wc = r.createWebClient()
|
||||
.withRedirectEnabled(false)
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
wc.login("user");
|
||||
Page p = wc.getPage(request);
|
||||
// currently the endpoint return a redirection to the previously visited page, none in our case
|
||||
// (so force no redirect to avoid false positive error)
|
||||
assertThat(p.getWebResponse().getStatusCode(), lessThan(400));
|
||||
|
||||
assertFalse(currentOne.getFuture().isCancelled());
|
||||
}
|
||||
{ // user with right can
|
||||
JenkinsRule.WebClient wc = r.createWebClient()
|
||||
.withRedirectEnabled(false)
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
wc.login("admin");
|
||||
Page p = wc.getPage(request);
|
||||
assertThat(p.getWebResponse().getStatusCode(), lessThan(400));
|
||||
|
||||
assertTrue(currentOne.getFuture().isCancelled());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package hudson.model;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import hudson.FilePath;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.model.Statement;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.RestartableJenkinsRule;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
//TODO after the security fix, it could be merged inside UserRestartTest
|
||||
public class UserRestartSEC897Test {
|
||||
|
||||
@Rule
|
||||
public RestartableJenkinsRule rr = new RestartableJenkinsRule();
|
||||
|
||||
@Test public void legacyConfigMoveCannotEscapeUserFolder() {
|
||||
rr.addStep(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
rr.j.jenkins.setSecurityRealm(rr.j.createDummySecurityRealm());
|
||||
assertThat(rr.j.jenkins.isUseSecurity(), equalTo(true));
|
||||
|
||||
// in order to create the folder "users"
|
||||
User.getById("admin", true).save();
|
||||
|
||||
{ // attempt with ".."
|
||||
JenkinsRule.WebClient wc = rr.j.createWebClient()
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml"));
|
||||
request.setAdditionalHeader("Authorization", base64("..", "any-password"));
|
||||
wc.getPage(request);
|
||||
}
|
||||
{ // attempt with "../users/.."
|
||||
JenkinsRule.WebClient wc = rr.j.createWebClient()
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml"));
|
||||
request.setAdditionalHeader("Authorization", base64("../users/..", "any-password"));
|
||||
wc.getPage(request);
|
||||
}
|
||||
|
||||
// security is still active
|
||||
assertThat(rr.j.jenkins.isUseSecurity(), equalTo(true));
|
||||
// but, the config file was moved
|
||||
FilePath rootPath = rr.j.jenkins.getRootPath();
|
||||
assertThat(rootPath.child("config.xml").exists(), equalTo(true));
|
||||
}
|
||||
});
|
||||
rr.addStep(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
assertThat(rr.j.jenkins.isUseSecurity(), equalTo(true));
|
||||
FilePath rootPath = rr.j.jenkins.getRootPath();
|
||||
assertThat(rootPath.child("config.xml").exists(), equalTo(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String base64(String login, String password) {
|
||||
return "Basic " + Base64.getEncoder().encodeToString((login + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -24,15 +24,26 @@
|
||||
|
||||
package hudson.model;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import hudson.FilePath;
|
||||
import hudson.tasks.Mailer;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.RestartableJenkinsRule;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class UserRestartTest {
|
||||
|
||||
@Rule
|
||||
@@ -81,4 +92,53 @@ public class UserRestartTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-897")
|
||||
public void legacyConfigMoveCannotEscapeUserFolder() {
|
||||
rr.addStep(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
rr.j.jenkins.setSecurityRealm(rr.j.createDummySecurityRealm());
|
||||
assertThat(rr.j.jenkins.isUseSecurity(), equalTo(true));
|
||||
|
||||
// in order to create the folder "users"
|
||||
User.getById("admin", true).save();
|
||||
|
||||
{ // attempt with ".."
|
||||
JenkinsRule.WebClient wc = rr.j.createWebClient()
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml"));
|
||||
request.setAdditionalHeader("Authorization", base64("..", "any-password"));
|
||||
wc.getPage(request);
|
||||
}
|
||||
{ // attempt with "../users/.."
|
||||
JenkinsRule.WebClient wc = rr.j.createWebClient()
|
||||
.withThrowExceptionOnFailingStatusCode(false);
|
||||
|
||||
WebRequest request = new WebRequest(new URL(rr.j.jenkins.getRootUrl() + "whoAmI/api/xml"));
|
||||
request.setAdditionalHeader("Authorization", base64("../users/..", "any-password"));
|
||||
wc.getPage(request);
|
||||
}
|
||||
|
||||
// security is still active
|
||||
assertThat(rr.j.jenkins.isUseSecurity(), equalTo(true));
|
||||
// but, the config file was moved
|
||||
FilePath rootPath = rr.j.jenkins.getRootPath();
|
||||
assertThat(rootPath.child("config.xml").exists(), equalTo(true));
|
||||
}
|
||||
});
|
||||
rr.addStep(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
assertThat(rr.j.jenkins.isUseSecurity(), equalTo(true));
|
||||
FilePath rootPath = rr.j.jenkins.getRootPath();
|
||||
assertThat(rootPath.child("config.xml").exists(), equalTo(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String base64(String login, String password) {
|
||||
return "Basic " + Base64.getEncoder().encodeToString((login + ":" + password).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2015, CloudBees, Inc
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package hudson.security;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.gargoylesoftware.htmlunit.util.Cookie;
|
||||
import hudson.security.pages.SignupPage;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
//TODO to be merged into HudsonPrivateSecurityRealm2Test after security release
|
||||
public class HudsonPrivateSecurityRealm2SEC1158Test {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule rule = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1158")
|
||||
public void singupNoLongerVulnerableToSessionFixation() throws Exception {
|
||||
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null);
|
||||
rule.jenkins.setSecurityRealm(securityRealm);
|
||||
JenkinsRule.WebClient wc = rule.createWebClient();
|
||||
|
||||
// to trigger the creation of a session
|
||||
wc.goTo("");
|
||||
Cookie sessionBefore = wc.getCookieManager().getCookie("JSESSIONID");
|
||||
String sessionIdBefore = sessionBefore.getValue();
|
||||
|
||||
SignupPage signup = new SignupPage(wc.goTo("signup"));
|
||||
signup.enterUsername("alice");
|
||||
signup.enterPassword("alice");
|
||||
signup.enterFullName("Alice User");
|
||||
signup.enterEmail("alice@nowhere.com");
|
||||
HtmlPage success = signup.submit(rule);
|
||||
assertThat(success.getElementById("main-panel").getTextContent(), containsString("Success"));
|
||||
assertThat(success.getAnchorByHref("/jenkins/user/alice").getTextContent(), containsString("Alice User"));
|
||||
|
||||
assertEquals("Alice User", securityRealm.getUser("alice").getDisplayName());
|
||||
|
||||
Cookie sessionAfter = wc.getCookieManager().getCookie("JSESSIONID");
|
||||
String sessionIdAfter = sessionAfter.getValue();
|
||||
|
||||
assertNotEquals(sessionIdAfter, sessionIdBefore);
|
||||
}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2015, CloudBees, Inc. and others
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package hudson.security;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlForm;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
|
||||
import com.gargoylesoftware.htmlunit.xml.XmlPage;
|
||||
import hudson.model.User;
|
||||
import jenkins.security.seed.UserSeedProperty;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.JenkinsRule.WebClient;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.xml.HasXPath.hasXPath;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@For({UserSeedProperty.class, HudsonPrivateSecurityRealm.class})
|
||||
public class HudsonPrivateSecurityRealmSEC1245Test {
|
||||
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1245")
|
||||
public void changingPassword_mustInvalidateAllSessions() throws Exception {
|
||||
User alice = prepareRealmAndAlice();
|
||||
String initialSeed = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
|
||||
WebClient wc = j.createWebClient();
|
||||
WebClient wc_anotherTab = j.createWebClient();
|
||||
|
||||
wc.login(alice.getId());
|
||||
assertUserConnected(wc, alice.getId());
|
||||
|
||||
wc_anotherTab.login(alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure");
|
||||
HtmlPasswordInput password1 = configurePage.getElementByName("user.password");
|
||||
HtmlPasswordInput password2 = configurePage.getElementByName("user.password2");
|
||||
|
||||
password1.setText("alice2");
|
||||
password2.setText("alice2");
|
||||
|
||||
HtmlForm form = configurePage.getFormByName("config");
|
||||
j.submit(form);
|
||||
|
||||
assertUserNotConnected(wc, alice.getId());
|
||||
assertUserNotConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
String seedAfter = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
assertThat(seedAfter, not(is(initialSeed)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1245")
|
||||
public void notChangingPassword_hasNoImpactOnSeed() throws Exception {
|
||||
User alice = prepareRealmAndAlice();
|
||||
String initialSeed = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
|
||||
WebClient wc = j.createWebClient();
|
||||
WebClient wc_anotherTab = j.createWebClient();
|
||||
|
||||
wc.login(alice.getId());
|
||||
assertUserConnected(wc, alice.getId());
|
||||
|
||||
wc_anotherTab.login(alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure");
|
||||
// not changing password this time
|
||||
HtmlForm form = configurePage.getFormByName("config");
|
||||
j.submit(form);
|
||||
|
||||
assertUserConnected(wc, alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
String seedAfter = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
assertThat(seedAfter, is(initialSeed));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1245")
|
||||
public void changingPassword_withSeedDisable_hasNoImpact() throws Exception {
|
||||
boolean previousConfig = UserSeedProperty.DISABLE_USER_SEED;
|
||||
try {
|
||||
UserSeedProperty.DISABLE_USER_SEED = true;
|
||||
|
||||
User alice = prepareRealmAndAlice();
|
||||
|
||||
WebClient wc = j.createWebClient();
|
||||
WebClient wc_anotherTab = j.createWebClient();
|
||||
|
||||
wc.login(alice.getId());
|
||||
assertUserConnected(wc, alice.getId());
|
||||
|
||||
wc_anotherTab.login(alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure");
|
||||
HtmlPasswordInput password1 = configurePage.getElementByName("user.password");
|
||||
HtmlPasswordInput password2 = configurePage.getElementByName("user.password2");
|
||||
|
||||
password1.setText("alice2");
|
||||
password2.setText("alice2");
|
||||
|
||||
HtmlForm form = configurePage.getFormByName("config");
|
||||
j.submit(form);
|
||||
|
||||
assertUserConnected(wc, alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
} finally {
|
||||
UserSeedProperty.DISABLE_USER_SEED = previousConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private User prepareRealmAndAlice() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(securityRealm);
|
||||
|
||||
return securityRealm.createAccount("alice", "alice");
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,10 @@ package hudson.security;
|
||||
import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import com.gargoylesoftware.htmlunit.util.Cookie;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlForm;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
|
||||
import com.gargoylesoftware.htmlunit.util.NameValuePair;
|
||||
import com.gargoylesoftware.htmlunit.xml.XmlPage;
|
||||
import hudson.ExtensionList;
|
||||
@@ -45,15 +47,16 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jenkins.security.ApiTokenProperty;
|
||||
import jenkins.security.SecurityListener;
|
||||
import jenkins.security.apitoken.ApiTokenPropertyConfiguration;
|
||||
import jenkins.security.apitoken.ApiTokenTestHelper;
|
||||
import jenkins.security.seed.UserSeedProperty;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.xml.HasXPath.hasXPath;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import jenkins.security.apitoken.ApiTokenPropertyConfiguration;
|
||||
import jenkins.security.SecurityListener;
|
||||
import jenkins.security.apitoken.ApiTokenTestHelper;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -61,6 +64,7 @@ import java.lang.reflect.Field;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.JenkinsRule.WebClient;
|
||||
@@ -70,6 +74,7 @@ import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
@For({UserSeedProperty.class, HudsonPrivateSecurityRealm.class})
|
||||
public class HudsonPrivateSecurityRealmTest {
|
||||
|
||||
@Rule
|
||||
@@ -605,4 +610,145 @@ public class HudsonPrivateSecurityRealmTest {
|
||||
assertThat(success.getElementById("main-panel").getTextContent(), not(containsString("Success")));
|
||||
assertThat(success.getElementById("main-panel").getTextContent(), containsString(regex));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1158")
|
||||
public void singupNoLongerVulnerableToSessionFixation() throws Exception {
|
||||
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(true, false, null);
|
||||
j.jenkins.setSecurityRealm(securityRealm);
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// to trigger the creation of a session
|
||||
wc.goTo("");
|
||||
Cookie sessionBefore = wc.getCookieManager().getCookie("JSESSIONID");
|
||||
String sessionIdBefore = sessionBefore.getValue();
|
||||
|
||||
SignupPage signup = new SignupPage(wc.goTo("signup"));
|
||||
signup.enterUsername("alice");
|
||||
signup.enterPassword("alice");
|
||||
signup.enterFullName("Alice User");
|
||||
signup.enterEmail("alice@nowhere.com");
|
||||
HtmlPage success = signup.submit(j);
|
||||
assertThat(success.getElementById("main-panel").getTextContent(), containsString("Success"));
|
||||
assertThat(success.getAnchorByHref("/jenkins/user/alice").getTextContent(), containsString("Alice User"));
|
||||
|
||||
assertEquals("Alice User", securityRealm.getUser("alice").getDisplayName());
|
||||
|
||||
Cookie sessionAfter = wc.getCookieManager().getCookie("JSESSIONID");
|
||||
String sessionIdAfter = sessionAfter.getValue();
|
||||
|
||||
assertNotEquals(sessionIdAfter, sessionIdBefore);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1245")
|
||||
public void changingPassword_mustInvalidateAllSessions() throws Exception {
|
||||
User alice = prepareRealmAndAlice();
|
||||
String initialSeed = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
|
||||
WebClient wc = j.createWebClient();
|
||||
WebClient wc_anotherTab = j.createWebClient();
|
||||
|
||||
wc.login(alice.getId());
|
||||
assertUserConnected(wc, alice.getId());
|
||||
|
||||
wc_anotherTab.login(alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure");
|
||||
HtmlPasswordInput password1 = configurePage.getElementByName("user.password");
|
||||
HtmlPasswordInput password2 = configurePage.getElementByName("user.password2");
|
||||
|
||||
password1.setText("alice2");
|
||||
password2.setText("alice2");
|
||||
|
||||
HtmlForm form = configurePage.getFormByName("config");
|
||||
j.submit(form);
|
||||
|
||||
assertUserNotConnected(wc, alice.getId());
|
||||
assertUserNotConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
String seedAfter = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
assertThat(seedAfter, not(is(initialSeed)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1245")
|
||||
public void notChangingPassword_hasNoImpactOnSeed() throws Exception {
|
||||
User alice = prepareRealmAndAlice();
|
||||
String initialSeed = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
|
||||
WebClient wc = j.createWebClient();
|
||||
WebClient wc_anotherTab = j.createWebClient();
|
||||
|
||||
wc.login(alice.getId());
|
||||
assertUserConnected(wc, alice.getId());
|
||||
|
||||
wc_anotherTab.login(alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure");
|
||||
// not changing password this time
|
||||
HtmlForm form = configurePage.getFormByName("config");
|
||||
j.submit(form);
|
||||
|
||||
assertUserConnected(wc, alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
String seedAfter = alice.getProperty(UserSeedProperty.class).getSeed();
|
||||
assertThat(seedAfter, is(initialSeed));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1245")
|
||||
public void changingPassword_withSeedDisable_hasNoImpact() throws Exception {
|
||||
boolean previousConfig = UserSeedProperty.DISABLE_USER_SEED;
|
||||
try {
|
||||
UserSeedProperty.DISABLE_USER_SEED = true;
|
||||
|
||||
User alice = prepareRealmAndAlice();
|
||||
|
||||
WebClient wc = j.createWebClient();
|
||||
WebClient wc_anotherTab = j.createWebClient();
|
||||
|
||||
wc.login(alice.getId());
|
||||
assertUserConnected(wc, alice.getId());
|
||||
|
||||
wc_anotherTab.login(alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
|
||||
HtmlPage configurePage = wc.goTo(alice.getUrl() + "/configure");
|
||||
HtmlPasswordInput password1 = configurePage.getElementByName("user.password");
|
||||
HtmlPasswordInput password2 = configurePage.getElementByName("user.password2");
|
||||
|
||||
password1.setText("alice2");
|
||||
password2.setText("alice2");
|
||||
|
||||
HtmlForm form = configurePage.getFormByName("config");
|
||||
j.submit(form);
|
||||
|
||||
assertUserConnected(wc, alice.getId());
|
||||
assertUserConnected(wc_anotherTab, alice.getId());
|
||||
} finally {
|
||||
UserSeedProperty.DISABLE_USER_SEED = previousConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private User prepareRealmAndAlice() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(securityRealm);
|
||||
|
||||
return securityRealm.createAccount("alice", "alice");
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package hudson.security;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.CookieManager;
|
||||
import com.gargoylesoftware.htmlunit.util.Cookie;
|
||||
import com.gargoylesoftware.htmlunit.xml.XmlPage;
|
||||
import hudson.model.User;
|
||||
import jenkins.security.seed.UserSeedProperty;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.xml.HasXPath.hasXPath;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@For(TokenBasedRememberMeServices2.class)
|
||||
public class TokenBasedRememberMeServices2SEC868Test {
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
@For(UserSeedProperty.class)
|
||||
public void rememberMeToken_invalid_afterUserSeedReset() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
User alice = realm.createAccount(username, username);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.login(username, username, true);
|
||||
CookieManager cm = wc.getCookieManager();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
assertUserConnected(wc, username);
|
||||
|
||||
alice.getProperty(UserSeedProperty.class).renewSeed();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
assertUserNotConnected(wc, username);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
@For(UserSeedProperty.class)
|
||||
public void rememberMeToken_stillValid_afterUserSeedReset_ifUserSeedDisabled() throws Exception {
|
||||
boolean currentStatus = UserSeedProperty.DISABLE_USER_SEED;
|
||||
try {
|
||||
UserSeedProperty.DISABLE_USER_SEED = true;
|
||||
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
User alice = realm.createAccount(username, username);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.login(username, username, true);
|
||||
CookieManager cm = wc.getCookieManager();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
assertUserConnected(wc, username);
|
||||
|
||||
alice.getProperty(UserSeedProperty.class).renewSeed();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
// as userSeed disabled, no care about the renew
|
||||
assertUserConnected(wc, username);
|
||||
} finally {
|
||||
UserSeedProperty.DISABLE_USER_SEED = currentStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
public void rememberMeToken_shouldNotAccept_expirationDurationLargerThanConfigured() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
TokenBasedRememberMeServices2 tokenService = (TokenBasedRememberMeServices2) realm.getSecurityComponents().rememberMe;
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
User alice = realm.createAccount(username, username);
|
||||
|
||||
{ // a malicious cookie with expiration too far in the future should not work
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we increase artificially the duration of validity, that could be used to have permanent access
|
||||
long oneDay = TimeUnit.DAYS.toMillis(1);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, oneDay, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// the application should not use the cookie to connect
|
||||
assertUserNotConnected(wc, username);
|
||||
}
|
||||
|
||||
{ // a hand crafted cookie with regular expiration duration works
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we reduce a bit the expiration date to simulate an "old" cookie (regular usage)
|
||||
long minusFiveMinutes = TimeUnit.MINUTES.toMillis(-5);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, minusFiveMinutes, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// if we reactivate the remember me feature, it's ok
|
||||
assertUserConnected(wc, username);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
public void rememberMeToken_skipExpirationCheck() throws Exception {
|
||||
boolean previousConfig = TokenBasedRememberMeServices2.SKIP_TOO_FAR_EXPIRATION_DATE_CHECK;
|
||||
try {
|
||||
TokenBasedRememberMeServices2.SKIP_TOO_FAR_EXPIRATION_DATE_CHECK = true;
|
||||
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
TokenBasedRememberMeServices2 tokenService = (TokenBasedRememberMeServices2) realm.getSecurityComponents().rememberMe;
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
User alice = realm.createAccount(username, username);
|
||||
|
||||
{ // a malicious cookie with expiration too far in the future should not work
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we increase artificially the duration of validity, that could be used to have permanent access
|
||||
long oneDay = TimeUnit.DAYS.toMillis(1);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, oneDay, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// the application should not use the cookie to connect
|
||||
assertUserConnected(wc, username);
|
||||
}
|
||||
|
||||
{ // a hand crafted cookie with regular expiration duration works
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we reduce a bit the expiration date to simulate an "old" cookie (regular usage)
|
||||
long minusFiveMinutes = TimeUnit.MINUTES.toMillis(-5);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, minusFiveMinutes, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// if we reactivate the remember me feature, it's ok
|
||||
assertUserConnected(wc, username);
|
||||
}
|
||||
} finally {
|
||||
TokenBasedRememberMeServices2.SKIP_TOO_FAR_EXPIRATION_DATE_CHECK = previousConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private Cookie createRememberMeCookie(TokenBasedRememberMeServices2 tokenService, long deltaDuration, User user) throws Exception {
|
||||
long tokenValiditySeconds = tokenService.getTokenValiditySeconds();
|
||||
long expiryTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(tokenValiditySeconds);
|
||||
|
||||
// the hack
|
||||
expiryTime += deltaDuration;
|
||||
|
||||
String signatureValue = tokenService.makeTokenSignature(expiryTime, user.getProperty(HudsonPrivateSecurityRealm.Details.class));
|
||||
String tokenValue = user.getId() + ":" + expiryTime + ":" + signatureValue;
|
||||
String tokenValueBase64 = new String(Base64.encodeBase64(tokenValue.getBytes()));
|
||||
return new Cookie(j.getURL().getHost(), tokenService.getCookieName(), tokenValueBase64);
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* The MIT License
|
||||
*
|
||||
* Copyright (c) 2018, CloudBees, Inc.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
package hudson.security;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.util.Cookie;
|
||||
import com.gargoylesoftware.htmlunit.xml.XmlPage;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.text.IsEmptyString.isEmptyString;
|
||||
import static org.hamcrest.xml.HasXPath.hasXPath;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class TokenBasedRememberMeServices2SEC996Test {
|
||||
@Rule
|
||||
public JenkinsRule j = new JenkinsRule();
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-996")
|
||||
public void rememberMeToken_shouldNotBeRead_ifOptionIsDisabled() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
|
||||
Cookie rememberMeCookie = null;
|
||||
{
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("alice", "alice", true);
|
||||
|
||||
|
||||
// we should see a remember me cookie
|
||||
rememberMeCookie = getRememberMeCookie(wc);
|
||||
assertNotNull(rememberMeCookie);
|
||||
assertThat(rememberMeCookie.getValue(), not(isEmptyString()));
|
||||
}
|
||||
|
||||
j.jenkins.setDisableRememberMe(true);
|
||||
{
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.getCookieManager().addCookie(rememberMeCookie);
|
||||
|
||||
// the application should not use the cookie to connect
|
||||
XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
|
||||
assertThat(page, hasXPath("//name", not(is("alice"))));
|
||||
}
|
||||
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
{
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.getCookieManager().addCookie(rememberMeCookie);
|
||||
|
||||
// if we reactivate the remember me feature, it's ok
|
||||
XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
|
||||
assertThat(page, hasXPath("//name", is("alice")));
|
||||
}
|
||||
}
|
||||
|
||||
private Cookie getRememberMeCookie(JenkinsRule.WebClient wc) {
|
||||
return wc.getCookieManager().getCookie(TokenBasedRememberMeServices2.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,45 @@
|
||||
package hudson.security;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.CookieManager;
|
||||
import com.gargoylesoftware.htmlunit.util.Cookie;
|
||||
import com.gargoylesoftware.htmlunit.xml.XmlPage;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.Arrays;
|
||||
import static java.util.logging.Level.FINEST;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jenkins.model.Jenkins;
|
||||
import jenkins.security.seed.UserSeedProperty;
|
||||
|
||||
import org.acegisecurity.Authentication;
|
||||
import org.acegisecurity.AuthenticationException;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.GrantedAuthority;
|
||||
import org.acegisecurity.GrantedAuthorityImpl;
|
||||
import org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices;
|
||||
import org.acegisecurity.userdetails.User;
|
||||
import org.acegisecurity.userdetails.UserDetails;
|
||||
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.For;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
import org.jvnet.hudson.test.LoggerRule;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.text.IsEmptyString.isEmptyString;
|
||||
import static org.hamcrest.xml.HasXPath.hasXPath;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
public class TokenBasedRememberMeServices2Test {
|
||||
|
||||
@Rule
|
||||
@@ -73,7 +90,7 @@ public class TokenBasedRememberMeServices2Test {
|
||||
@Override
|
||||
protected UserDetails authenticate(String username, String password) throws AuthenticationException {
|
||||
if (username.equals(password)) {
|
||||
return new User(username, password, true, new GrantedAuthority[] {new GrantedAuthorityImpl("myteam")});
|
||||
return new org.acegisecurity.userdetails.User(username, password, true, new GrantedAuthority[] {new GrantedAuthorityImpl("myteam")});
|
||||
}
|
||||
throw new BadCredentialsException(username);
|
||||
}
|
||||
@@ -127,4 +144,212 @@ public class TokenBasedRememberMeServices2Test {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
@For(UserSeedProperty.class)
|
||||
public void rememberMeToken_invalid_afterUserSeedReset() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
hudson.model.User alice = realm.createAccount(username, username);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.login(username, username, true);
|
||||
CookieManager cm = wc.getCookieManager();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
assertUserConnected(wc, username);
|
||||
|
||||
alice.getProperty(UserSeedProperty.class).renewSeed();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
assertUserNotConnected(wc, username);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
@For(UserSeedProperty.class)
|
||||
public void rememberMeToken_stillValid_afterUserSeedReset_ifUserSeedDisabled() throws Exception {
|
||||
boolean currentStatus = UserSeedProperty.DISABLE_USER_SEED;
|
||||
try {
|
||||
UserSeedProperty.DISABLE_USER_SEED = true;
|
||||
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
hudson.model.User alice = realm.createAccount(username, username);
|
||||
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.login(username, username, true);
|
||||
CookieManager cm = wc.getCookieManager();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
assertUserConnected(wc, username);
|
||||
|
||||
alice.getProperty(UserSeedProperty.class).renewSeed();
|
||||
|
||||
cm.removeCookie(cm.getCookie("JSESSIONID"));
|
||||
// as userSeed disabled, no care about the renew
|
||||
assertUserConnected(wc, username);
|
||||
} finally {
|
||||
UserSeedProperty.DISABLE_USER_SEED = currentStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
public void rememberMeToken_shouldNotAccept_expirationDurationLargerThanConfigured() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
TokenBasedRememberMeServices2 tokenService = (TokenBasedRememberMeServices2) realm.getSecurityComponents().rememberMe;
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
hudson.model.User alice = realm.createAccount(username, username);
|
||||
|
||||
{ // a malicious cookie with expiration too far in the future should not work
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we increase artificially the duration of validity, that could be used to have permanent access
|
||||
long oneDay = TimeUnit.DAYS.toMillis(1);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, oneDay, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// the application should not use the cookie to connect
|
||||
assertUserNotConnected(wc, username);
|
||||
}
|
||||
|
||||
{ // a hand crafted cookie with regular expiration duration works
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we reduce a bit the expiration date to simulate an "old" cookie (regular usage)
|
||||
long minusFiveMinutes = TimeUnit.MINUTES.toMillis(-5);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, minusFiveMinutes, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// if we reactivate the remember me feature, it's ok
|
||||
assertUserConnected(wc, username);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-868")
|
||||
public void rememberMeToken_skipExpirationCheck() throws Exception {
|
||||
boolean previousConfig = TokenBasedRememberMeServices2.SKIP_TOO_FAR_EXPIRATION_DATE_CHECK;
|
||||
try {
|
||||
TokenBasedRememberMeServices2.SKIP_TOO_FAR_EXPIRATION_DATE_CHECK = true;
|
||||
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
|
||||
HudsonPrivateSecurityRealm realm = new HudsonPrivateSecurityRealm(false, false, null);
|
||||
TokenBasedRememberMeServices2 tokenService = (TokenBasedRememberMeServices2) realm.getSecurityComponents().rememberMe;
|
||||
j.jenkins.setSecurityRealm(realm);
|
||||
|
||||
String username = "alice";
|
||||
hudson.model.User alice = realm.createAccount(username, username);
|
||||
|
||||
{ // a malicious cookie with expiration too far in the future should not work
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we increase artificially the duration of validity, that could be used to have permanent access
|
||||
long oneDay = TimeUnit.DAYS.toMillis(1);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, oneDay, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// the application should not use the cookie to connect
|
||||
assertUserConnected(wc, username);
|
||||
}
|
||||
|
||||
{ // a hand crafted cookie with regular expiration duration works
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
// by default we have 14 days of validity,
|
||||
// here we reduce a bit the expiration date to simulate an "old" cookie (regular usage)
|
||||
long minusFiveMinutes = TimeUnit.MINUTES.toMillis(-5);
|
||||
Cookie cookie = createRememberMeCookie(tokenService, minusFiveMinutes, alice);
|
||||
wc.getCookieManager().addCookie(cookie);
|
||||
|
||||
// if we reactivate the remember me feature, it's ok
|
||||
assertUserConnected(wc, username);
|
||||
}
|
||||
} finally {
|
||||
TokenBasedRememberMeServices2.SKIP_TOO_FAR_EXPIRATION_DATE_CHECK = previousConfig;
|
||||
}
|
||||
}
|
||||
|
||||
private Cookie createRememberMeCookie(TokenBasedRememberMeServices2 tokenService, long deltaDuration, hudson.model.User user) throws Exception {
|
||||
long tokenValiditySeconds = tokenService.getTokenValiditySeconds();
|
||||
long expiryTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(tokenValiditySeconds);
|
||||
|
||||
// the hack
|
||||
expiryTime += deltaDuration;
|
||||
|
||||
String signatureValue = tokenService.makeTokenSignature(expiryTime, user.getProperty(HudsonPrivateSecurityRealm.Details.class));
|
||||
String tokenValue = user.getId() + ":" + expiryTime + ":" + signatureValue;
|
||||
String tokenValueBase64 = new String(Base64.encodeBase64(tokenValue.getBytes()));
|
||||
return new Cookie(j.getURL().getHost(), tokenService.getCookieName(), tokenValueBase64);
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-996")
|
||||
public void rememberMeToken_shouldNotBeRead_ifOptionIsDisabled() throws Exception {
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
|
||||
|
||||
Cookie rememberMeCookie = null;
|
||||
{
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.login("alice", "alice", true);
|
||||
|
||||
|
||||
// we should see a remember me cookie
|
||||
rememberMeCookie = getRememberMeCookie(wc);
|
||||
assertNotNull(rememberMeCookie);
|
||||
assertThat(rememberMeCookie.getValue(), not(isEmptyString()));
|
||||
}
|
||||
|
||||
j.jenkins.setDisableRememberMe(true);
|
||||
{
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.getCookieManager().addCookie(rememberMeCookie);
|
||||
|
||||
// the application should not use the cookie to connect
|
||||
XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
|
||||
assertThat(page, hasXPath("//name", not(is("alice"))));
|
||||
}
|
||||
|
||||
j.jenkins.setDisableRememberMe(false);
|
||||
{
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
wc.getCookieManager().addCookie(rememberMeCookie);
|
||||
|
||||
// if we reactivate the remember me feature, it's ok
|
||||
XmlPage page = (XmlPage) wc.goTo("whoAmI/api/xml", "application/xml");
|
||||
assertThat(page, hasXPath("//name", is("alice")));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user