In my previous post I presented the basics of sharing sessions in a cluster by storing session data in a client-side cookie. In part 2, I’ll talk about the security aspects of this client-side cookie store, i.e. how to protect it from security threats.

To prevent attacks specific to client-side sessions, I’ll add encryption, signing, and session timeout to the code. In addition, I’ll talk about solutions to protect against security threats common to any web application, such as Session Hijacking, Session Replay, and Cross-Site Scripting. The result will be an implementation of the Session-In-Cookie pattern that allows simple and secure session-sharing in a cluster.

Disclaimer: any hand-rolled security solution always bears risks. As stated on the Open Web Application Security Project (OWASP) website, “Developers frequently build custom authentication and session management schemes, but building these correctly is hard. As a result, these custom schemes frequently have flaws (…)“. I am not a security expert, and the following solution may have flaws, so proceed with caution. If you use this solution in a production environment be sure to have a security check conducted by an independent party.

Encryption

The code presented in part 1 accomplished our main objective – sharing session data by storing it in a cookie – but did not provide any security against malicious hackers trying to exploit data. Even though object serialisation does not lead to a human-readable form – the session attributes or their values cannot be inspected by viewing the cookie – the cookie could easily be converted into an object model to be read and tampered with.

If a hacker has obtained a valid session he can therefore manipulate the session data, e.g. by copying the cookie from the browser’s cookie manager, changing it, and pasting the changed cookie back into the cookie manager. To prevent this manipulation encryption can be used to make the contents of the session unreadable.

Adding encryption to our solution means encrypting the serialised session before storing the data in the cookie.

private void storeSessionInCookie(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { (...) CookieSession cookieSession = new CookieSession(); cookieSession.setAttributes(attributes); try { String serialisedCookieSession = toString((Serializable) cookieSession); Cookie newSessionCookie = new Cookie("session", encrypt(serialisedCookieSession)); newSessionCookie.setPath("/"); httpServletResponse.addCookie(newSessionCookie); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } }

The encryption is done using a standard AES encryption.

private static String SECRET = "MySuperSecretKey"; // secret key length must be 16 (...) public String encrypt(String string) { byte[] ciphertext = null; try { SecretKey key = new SecretKeySpec(SECRET.getBytes(), "AES"); Cipher aes = Cipher.getInstance("AES/ECB/PKCS5Padding"); aes.init(Cipher.ENCRYPT_MODE, key); ciphertext = aes.doFinal(string.getBytes()); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } return new String(ciphertext); }

To restore the session from the cookie, the serialised session string has to be decrypted first before it can be de-serialised to our CookieSession object.

private void getSessionFromCookie(HttpServletRequest httpServletRequest) throws IOException { (...) if (null != sessionCookie) { String encryptedSession = sessionCookie.getValue(); try { String serialisedSession = decrypt(encryptedSession); CookieSession cookieSession = (CookieSession) fromString(serialisedSession); Map<String, Object> attributes = cookieSession.getAttributes(); for (Map.Entry<String, Object> attribute : attributes.entrySet()) { session.setAttribute(attribute.getKey(), attribute.getValue()); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } }

The decryption just reverses the previously used encryption method.

public String decrypt(String encryptedString) { String cleartext=null; try { SecretKey key = new SecretKeySpec(SECRET.getBytes(), "AES"); Cipher aes = Cipher.getInstance("AES/ECB/PKCS5Padding"); aes.init(Cipher.DECRYPT_MODE, key); cleartext = new String(aes.doFinal(encryptedString.getBytes())); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } return cleartext; }

With this modified session filter the cookie sent to the browser is not readable anymore. Even if the method of serialisation and encryption is known, an attacker cannot read the data unless the private key is compromised.

Signing

Even though once the session is encrypted the session cannot be deciphered, it can still be tampered with. A hacker could change the encrypted cookie string, and even though he cannot know what kind of effect this would have to the session data, it could still be a valid session cookie. This means tampering is possible.

To prevent session tampering a signature hash of the serialised cookieSession object can be added before encryption. This hash gets encrypted together with the session object to form a single cookie string. If a hacker changes that cookie string he either changes the content, the signature hash, or both and this would reveal that the cookie has been tampered with.

The signature is added once the cookieSession has been serialised. The serialised cookieSession string plus the signature are then encrypted together.

private void storeSessionInCookie(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { (...) CookieSession cookieSession = new CookieSession(); cookieSession.setAttributes(attributes); try { String serialisedCookieSession = toString((Serializable) cookieSession); String signature = calculateSignature(serialisedCookieSession); Cookie newSessionCookie = new Cookie("session", encrypt(serialisedCookieSession+signature)); newSessionCookie.setPath("/"); httpServletResponse.addCookie(newSessionCookie); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } }

The signature is a standard SHA-1 hash padded to 40 characters to have a uniform signature length.

private String calculateSignature(String serialisedSession) { String hash = null; MessageDigest cript; try { cript = MessageDigest.getInstance("SHA-1"); cript.reset(); cript.update(serialisedSession.getBytes("utf8")); hash = new BigInteger(1, cript.digest()).toString(16); // Pad to 40 chars hash = String.format("%1$40s", hash); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } return hash; }

When restoring the session, the signature is checked after the session cookie is decrypted. The signature is always the same length, so the last 40 characters of the decrypted string is our signature hash.

private void getSessionFromCookie(HttpServletRequest httpServletRequest) throws IOException { (...) String encryptedSessionPlusSignature = sessionCookie.getValue(); try { String serialisedSessionPlusSignature = decrypt(encryptedSessionPlusSignature); String signature = serialisedSessionPlusSignature.substring(serialisedSessionPlusSignature.length() - 40); String serialisedSession = serialisedSessionPlusSignature.substring(0, serialisedSessionPlusSignature.length() - 40); // check signature if (!signature.equals(calculateSignature(serialisedSession))) { LOGGER.error("Session has been tampered with"); } CookieSession cookieSession = (CookieSession) fromString(serialisedSession); Map<String, Object> attributes = cookieSession.getAttributes(); for (Map.Entry<String, Object> attribute : attributes.entrySet()) { session.setAttribute(attribute.getKey(), attribute.getValue()); } } catch (Exception e) { LOGGER.error(e.getMessage(), e); } } }

Session Timeout

Sessions usually have a session timeout to expire the session after certain amount of inactivity. For servlets this timeout can be set in the web.xml. One reason for this timeout is to limit the amount of memory needed to store session data: once a session is expired the data can be removed from memory.

There is also a security-relevant reason for such a timeout: to limit the time hackers have to exploit an existing session. An example for this scenario is the use of a public computer. If a legitimate user of your application forgets to log out and leaves the public computer, the next user – possibly a hacker – has a valid session to your application, and can use your application on behalf of the legitimate user. Without a session timeout this scenario could happen any time after your legitimate user has left, which could even be days later if the computer is left running .With session timeout a hacker only has a short period of time where he can do damage.

To incorporate session timeout we add a lastAccessedTime member to the CookieSession class. It represents the time of last activity, and is set every time the cookie is written.

private long lastAccessedTime;

The lastAccessTime is set before the CookieSession object is serialised.

private void storeSessionInCookie(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { (...) cookieSession.setAttributes(attributes); cookieSession.setLastAccessedTime(new Date().getTime()); try { String serialisedCookieSession = toString((Serializable) cookieSession); String signature = calculateSignature(serialisedCookieSession); Cookie newSessionCookie = new Cookie("session", encrypt(serialisedCookieSession+signature)); newSessionCookie.setPath("/"); httpServletResponse.addCookie(newSessionCookie); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } }

On the return-trip the lastAccessedTime is checked after the cookie is de-serialised again. The session timeout specified in the web.xml (session.getMaxActiveInterval()) is added to lastAccessedTime and compared to the current time. If the current time is greater then the session has expired.

private void getSessionFromCookie(HttpServletRequest httpServletRequest) throws IOException { (...) String serialisedSession = decrypt(encryptedSession); CookieSession cookieSession = (CookieSession) fromString(serialisedSession); // Check Session expiry if (cookieSession.getLastAccessedTime() + (session.getMaxInactiveInterval() * 1000) < new Date() .getTime()) { LOGGER.error("Session expired"); // TODO: Forward to Expiration page return; } (...) }

Other Security Threats

Adding encryption, signing, and session timeout sets a basic level of security aimed at attackers that exploit the fact that the session is stored client-side. In addition, there are security threats any web application has to be protected form. Here are three more common threats and how they apply to cookie-based session storage.

Session Hijacking

Session hijacking is an exploitation by which the attacker sniffs out the session cookie of a legitimate user to get unauthorised access to the application. Session hijacking is always possible if the traffic is not secure, regardless of whether the session data is stored server- or client-side. If the data is stored server-side, the cookie will only contain the session id. If the data is stored client-side, the cookie contains the complete session data. Either way, if an attacker gets a hold of this cookie he can get access to the application.

The best way to prevent session hijacking is to use Transport-Layer Security (TLS). To ensure that the session cookie is only transmitted via https the cookie should be marked as secure:

newSessionCookie.setSecure(true);

Replay Attack

TLS also protects from Replay Attacks, by which traffic between client and server is recorded to replay it later on. TLS adds a one-time key to the conversation to reject if the same traffic is sent multiple times.

There is a variant of the Replay Attack that only is possible if the session is stored client-side. A hacker who has legitimate session into you application can reset a session to its earlier state by just copy-and-pasting and older value into the cookie. This value will be valid, because it is properly encrypted and signed, and if the session hasn’t expired the application will accept this old session without knowing of any tampering.

This cannot easily be prevented with client-side sessions, but will only cause problems if certain information is kept in the session. An example would be if an account balance is kept in the session, and the hacker buys an item on your web site. If the cost of the item is deducted from the account balance in the session, then copy-and-pasting an older value into the cookie will reset this account balance. The rule-of-thumb is that state-changing information should not be stored in the session.

Cross-site Scripting

Cross-Site Scripting (XSS) is an exploitation by which the session cookie is sniffed out using client-side script injected into the application. Again, the solution to prevent these types of attacks are the same as with any web-application. In both cases marking the cookie as http-only will prevent any script from accessing browser cookies.

newSessionCookie.setHttpOnly(true);

Conclusion

In Part 1, I explained how to share session data by storing it in a client-side cookie. In this post I’ve added security measures to this cookie store. Together, we’ve ended up with a session-sharing solution that is simple to implement, transparent to the developer, and reasonably secure. It is therefore a viable alternative to other session-sharing solutions for clustered environments. This solution passed a 3rd-party security check, and was successfully used in a large-scale public-facing web application running on a Tomcat cluster to enable Continuous Delivery without outage to the end-user.

The complete source code including the session timeout, encryption, and signing of the cookie can be found on Github.