From 73d7e7e3884a54c29bcbe3e9d98fd0f5d04e3930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20Ayd=C4=B1n?= Date: Tue, 25 Jul 2017 09:54:32 +0300 Subject: [PATCH] Fix Spring session fixation&stale session issues (#52) Two problems solved: - Fix session fixation vulnerability - Fix stale Hazelcast session issue Session fixation vulnerability occurred with Spring Security MVC as it used isRequestedSessionIdValid method on HttpServletRequest interface which was not implemented before this commit. Stale Hazelcast session issue occurs when a request comes in with a valid JSESSIONID and a hazelcast.sessionId that corresponds to another Hazelcast session. In this case, we just used the existing session and used the incoming JSESSIONID to find the corresponding hazelcast.sessionId and Hazelcast session. Now we let the incoming hazelcast.sessionId to override the hazelcast.sessionId that corresponds to the request's JSESSIONID. Fix #47 --- .../java/com/hazelcast/web/WebFilter.java | 14 +++ .../test/spring/SpringAwareWebFilterTest.java | 103 ++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/src/main/java/com/hazelcast/web/WebFilter.java b/src/main/java/com/hazelcast/web/WebFilter.java index 52bac0c..4ee458a 100644 --- a/src/main/java/com/hazelcast/web/WebFilter.java +++ b/src/main/java/com/hazelcast/web/WebFilter.java @@ -364,7 +364,13 @@ public HazelcastHttpSession getSession(final boolean create) { return hazelcastSession; } + @Override + public boolean isRequestedSessionIdValid() { + return hazelcastSession != null && hazelcastSession.isValid(); + } + private HazelcastHttpSession readSessionFromLocal() { + // following chunk is executed _only_ when session is invalidated and getSession is called on the request String invalidatedOriginalSessionId = null; if (hazelcastSession != null && !hazelcastSession.isValid()) { LOGGER.finest("Session is invalid!"); @@ -374,9 +380,17 @@ private HazelcastHttpSession readSessionFromLocal() { } else if (hazelcastSession != null) { return hazelcastSession; } + HttpSession originalSession = getOriginalSession(false); if (originalSession != null) { String hazelcastSessionId = originalSessions.get(originalSession.getId()); + String hazelcastSessionIdFromRequest = findHazelcastSessionIdFromRequest(); + // hazelcast.sessionId from the request overrides hazelcast.sessionId corresponding to jsessionid from + // the request + if (hazelcastSessionIdFromRequest != null && !hazelcastSessionIdFromRequest.equals(hazelcastSessionId)) { + hazelcastSessionId = hazelcastSessionIdFromRequest; + } + if (hazelcastSessionId != null) { hazelcastSession = getSessionWithId(hazelcastSessionId); diff --git a/src/test/java/com/hazelcast/wm/test/spring/SpringAwareWebFilterTest.java b/src/test/java/com/hazelcast/wm/test/spring/SpringAwareWebFilterTest.java index a5f48d0..172eadb 100644 --- a/src/test/java/com/hazelcast/wm/test/spring/SpringAwareWebFilterTest.java +++ b/src/test/java/com/hazelcast/wm/test/spring/SpringAwareWebFilterTest.java @@ -20,7 +20,10 @@ import com.hazelcast.test.annotation.QuickTest; import com.hazelcast.wm.test.ServletContainer; import com.hazelcast.wm.test.TomcatServer; +import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.cookie.Cookie; +import org.junit.Assert; import org.junit.Test; import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; @@ -28,6 +31,7 @@ import org.springframework.security.core.session.SessionRegistry; import java.util.Iterator; +import java.util.List; import java.util.Set; import static org.junit.Assert.assertEquals; @@ -43,6 +47,83 @@ protected ServletContainer getServletContainer(int port, String sourceDir, Strin return new TomcatServer(port, sourceDir, serverXml); } + // https://github.com/hazelcast/hazelcast-wm/issues/47 + @Test + public void testSessionFixationProtectionLostTomcatSessionId() throws Exception { + // Scenario: An initial request is made to the server before authentication that creates a tomcat session ID and + // a hazlecast session ID (e.g. a login page). Next, an authentication request is made but only the Hazelcast + // session ID is provided. It is expected that the original hazlecast session should be destroyed. + + // Create a session so that a Tomcat and Hazelcast session ID is created + SpringSecuritySession sss = createSession(null, this.serverPort1); + + // Remove the Tomcat session ID cookie from the request + List cookies = sss.cookieStore.getCookies(); + sss.cookieStore.clear(); + for (Cookie cookie : cookies) { + if (!SESSION_ID_COOKIE_NAME.equals(cookie.getName())) { + sss.cookieStore.addCookie(cookie); + } + } + + String originalHazelcastSessionId = sss.getHazelcastSessionId(); + + // Login with only the Hazelcast session ID provided + sss = login(sss, false); + + String hazelcastSessionId = sss.getHazelcastSessionId(); + + // Verify that the original hazelcast session ID was invalidated + assertNotEquals(originalHazelcastSessionId, hazelcastSessionId); + } + + // https://github.com/hazelcast/hazelcast-wm/issues/47 + @Test + public void testStaleLocalCache() throws Exception { + // Scenario: There are two server nodes (1 & 2) behind a load balancer. Each node handles a request prior to + // authentication so that both nodes have the Hazlecast session ID cached locally against a Tomcat session ID. + // Say node '1' performs the authentication on the login request. Node '2' should not attempt to use the + // original unauthenticated hazelcast session that was destroyed by node '1'. + + // Create initial session on node 1 + SpringSecuritySession sss = createSession(null, this.serverPort1); + + // Get the cookies for the initial request to node 1 + Cookie node1InitialTomcatCookie = getCookie(sss, SESSION_ID_COOKIE_NAME); + Cookie hazelcastCookiePreAuthentication = getCookie(sss, HZ_SESSION_ID_COOKIE_NAME); + + // Make a request to node 2 with the hazelcast session ID + sss.cookieStore.clear(); + sss.cookieStore.addCookie(hazelcastCookiePreAuthentication); + request("hello.jsp", this.serverPort2, sss.cookieStore); + + // Get the tomcat cookie for node 2 + Cookie node2InitialTomcatCookie = getCookie(sss, SESSION_ID_COOKIE_NAME); + + // Login using node 1 + sss.cookieStore.clear(); + sss.cookieStore.addCookie(hazelcastCookiePreAuthentication); + sss.cookieStore.addCookie(node1InitialTomcatCookie); + + sss = login(sss, false); + + // Get the new hazelcast cookie + Cookie hazelcastAuthPostAuthentication = getCookie(sss, HZ_SESSION_ID_COOKIE_NAME); + + HttpResponse node1Response = request("hello.jsp", this.serverPort1, sss.cookieStore); + // Request should not be re-directed to login + Assert.assertNotEquals(302, node1Response.getStatusLine().getStatusCode()); + + // Make a request to node 2 + sss.cookieStore.clear(); + sss.cookieStore.addCookie(node2InitialTomcatCookie); + sss.cookieStore.addCookie(hazelcastAuthPostAuthentication); + + HttpResponse node2Response = request("hello.jsp", this.serverPort2, sss.cookieStore); + // Request should not be re-directed to login + Assert.assertNotEquals(302, node2Response.getStatusLine().getStatusCode()); + } + @Test public void test_issue_3049() throws Exception { Set applicationContextSet = @@ -106,4 +187,26 @@ public void testChangeSessionIdAfterLogin() throws Exception { assertNotEquals(jsessionIdBeforeLogin, sss.getSessionId()); assertNotEquals(hzSessionIdBeforeLogin, sss.getHazelcastSessionId()); } + + private Cookie getCookie(final SpringSecuritySession sss, final String cookieName) { + if (sss.cookieStore.getCookies() != null) { + for (org.apache.http.cookie.Cookie cookie : sss.cookieStore.getCookies()) { + if (cookie.getName().equals(cookieName)) { + return cookie; + } + } + } + return null; + } + + private SpringSecuritySession createSession(SpringSecuritySession springSecuritySession, final int serverPort) + throws Exception { + if (springSecuritySession == null) { + springSecuritySession = new SpringSecuritySession(); + } + + request(RequestType.POST_REQUEST, SPRING_SECURITY_LOGIN_URL, serverPort, springSecuritySession.cookieStore); + + return springSecuritySession; + } }