diff --git a/test/src/test/java/hudson/PluginSEC925Test.java b/test/src/test/java/hudson/PluginSEC925Test.java deleted file mode 100644 index ab3fb94243..0000000000 --- a/test/src/test/java/hudson/PluginSEC925Test.java +++ /dev/null @@ -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> pluginInstalled = r.jenkins.pluginManager.install(Arrays.asList("credentials"), true); - - for (Future job : pluginInstalled) { - job.get(); - } - r.createWebClient().assertFails("plugin/credentials/.timestamp2", HttpServletResponse.SC_BAD_REQUEST); - } -} diff --git a/test/src/test/java/hudson/PluginTest.java b/test/src/test/java/hudson/PluginTest.java index 17f7329e00..d3301f3dfa 100644 --- a/test/src/test/java/hudson/PluginTest.java +++ b/test/src/test/java/hudson/PluginTest.java @@ -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> pluginInstalled = r.jenkins.pluginManager.install(Arrays.asList("credentials"), true); + + for (Future job : pluginInstalled) { + job.get(); + } + r.createWebClient().assertFails("plugin/credentials/.timestamp2", HttpServletResponse.SC_BAD_REQUEST); + } } diff --git a/test/src/test/java/hudson/model/QueueSEC891Test.java b/test/src/test/java/hudson/model/QueueSEC891Test.java deleted file mode 100644 index 573065ae7b..0000000000 --- a/test/src/test/java/hudson/model/QueueSEC891Test.java +++ /dev/null @@ -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 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()); - } - } -} diff --git a/test/src/test/java/hudson/model/QueueTest.java b/test/src/test/java/hudson/model/QueueTest.java index ed550b06be..63d6a1deb1 100644 --- a/test/src/test/java/hudson/model/QueueTest.java +++ b/test/src/test/java/hudson/model/QueueTest.java @@ -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 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()); + } + } } diff --git a/test/src/test/java/hudson/model/UserRestartSEC897Test.java b/test/src/test/java/hudson/model/UserRestartSEC897Test.java deleted file mode 100644 index 4763504de0..0000000000 --- a/test/src/test/java/hudson/model/UserRestartSEC897Test.java +++ /dev/null @@ -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)); - } -} diff --git a/test/src/test/java/hudson/model/UserRestartTest.java b/test/src/test/java/hudson/model/UserRestartTest.java index 2f6cec0750..d1d8253064 100644 --- a/test/src/test/java/hudson/model/UserRestartTest.java +++ b/test/src/test/java/hudson/model/UserRestartTest.java @@ -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)); + } } diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealm2SEC1158Test.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealm2SEC1158Test.java deleted file mode 100644 index fca41a931d..0000000000 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealm2SEC1158Test.java +++ /dev/null @@ -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); - } -} diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmSEC1245Test.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmSEC1245Test.java deleted file mode 100644 index 1f2eece510..0000000000 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmSEC1245Test.java +++ /dev/null @@ -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)))); - } -} diff --git a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java index 557dbbea4e..dec921ea24 100644 --- a/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java +++ b/test/src/test/java/hudson/security/HudsonPrivateSecurityRealmTest.java @@ -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)))); + } } diff --git a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2SEC868Test.java b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2SEC868Test.java deleted file mode 100644 index 8f8031b79b..0000000000 --- a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2SEC868Test.java +++ /dev/null @@ -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)))); - } -} diff --git a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2SEC996Test.java b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2SEC996Test.java deleted file mode 100644 index 1abecfbfc8..0000000000 --- a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2SEC996Test.java +++ /dev/null @@ -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); - } -} diff --git a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java index 35bea92686..e98eb4354a 100644 --- a/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java +++ b/test/src/test/java/hudson/security/TokenBasedRememberMeServices2Test.java @@ -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"))); + } + } }