JSESSION, New SameSite cookie policy in Google Chrome and Spring

Let me start the discussion like this. In past few days have you noticed any sudden drops of conversions in your eCommerce website? Or else are you getting frequent complaints from your eCommerce site customers saying that they had drop out of the shopping cart process after they have completed payments? Have you experiencing issues in your web application where you consume 3rd party services in specific browsers? Some of the contents which was loading within iFrames are now broken?

source : giphy.com

Then it will be worth to continue reading this article by dedicating part of your valuable time.

To describe the issue further, you might experiencing difficulties/issues in your website in the instance where you consume a 3rd party service and they have redirect the client back to your site, or else you might not be able to load the content of a 3rd party website which was working perfectly until recent. At the time of I’m writing this, the issue is only noticeable and can be occurred in Google Chrome, Chromium with their latest version (>80). But if you are reading this after some time since I have wrote this, then the issue could be noticed in the other mainstream browsers as well.

NOTE : I have tried almost all the solutions out there in the internet but nothing works or solve my problem.

But Why?

With the recent versions of the Google Chrome browser, they have imposed a new “secure-by-defaultcookie model on their plans to make more secure their browsers with online browsing activities.

source : giphy.com

Basically cookies are used in web context in order to track the state of the online users or to provide more customized and better version of experience to the end users by tracking their session and online identity. Even though sharing this cookie with 3rd party is not a bad thing it has the potential for misuse of those cookies and attack the end user. Until the new cookie model was revealed by Chrome, it was allowing to access all the cookies in the browser by 3rd party websites by default. The change is targeting preventing online users from Cross-Site-Request-forgery (CSRF) attacks where hackers can intrude innocent online users by hijacking the cookies and exploit them using session surfing or one click attacks. So chrome wants to make you more secure when your application is making cross-site requests through the browser. If you are still not familiar with the terminologies cookies, CSRF and other stuffs their are plenty of resources in the web to read and even you can click on the particular terms in this article and read them in where I recommend.

The new Cookie model

With the New changes, Google Chrome (Hopefully other browsers will follow the same in later phases), expecting site owners to set couple of new cookie attributes with their site’s session cookies called SameSite and Secure. Let’s discuss about them one by one.

SameSite

With this attribute, site owners will get the ability to control how their cookies are shared with other sites. This attribute can be set with 3 different values as follows.

source : blog.heroku.com
  1. Strict – If SameSite=Strict was set, then the cookies will be shared only within first party context which is only within your own domain, regardless of the Request method which has been used. So browser will not pass the cookies with cross site requests.
  2. Lax (default) – If none of values are set for SameSite attribute then browser will fallback it for this value. With Lax, browser will pass the cookies with cross-site requests only through top-level navigation (GET). So cookies will not be passed along with cross-site requests which was made through HttpMethods other than GET (like POST).
  3. None – Cookies are allowed to pass through all Cross-site requests regardless of the navigation method (GET, POST,… allowed). But this requires to send only through a secured connection (through https and http won’t work) so this required to set Secure cookie attribute as well.

Secure

This attribute enables passing the cookies only through a secured and encrypted connection which requires a HTTPS connection.

What will happen without SameSite?

Let’s try to understand this with an sample scenario.

Let’s assume that I have an eCommerce web application with MVC pattern and the 3rd party service is a payment gateway provider. once customer completes the shopping and going to purchase the items in the shopping cart, I’m redirecting the customer to an external payment gateway through an Direct Hosted Pay Page (DHPP, or else this can be loaded within an iFrame as well). Once customer completed the payment, payment gateway will redirect the client through a browser redirection back to my application then my web application logic decides to show a confirmation page or redirect back to shopping cart based on the payment status.

Assumptions

  1. The communication between the application server and the payment gateway is a server to server communication.
  2. Payment gateway response URLs are allowed/permitted without authentication from the Application security configurations.
UML Sequence diagram for Sample Ecommerce Payment process

As SameSite attribute is not set in here, the browser will fallback to it’s default SameSite value with Lax. If the cross-site request #6 is a GET request then the cookie will be passed with the request to the application server and it will identify the client session. No problem. If the request #6 is a POST request the session cookie(cookie with JSESSIONID in Java) will not pass along with the request because SameSite=Lax only allows requests with GET method to pass through. So application server unable to identify the client/session and various issues might arise depends on the application’s logic.

source : giphy.com

common issues

  1. As session is lost, Application (Servlet container in Java) will create a new session. With Spring Security, it will append the JSESSIONID at the end of the URL as an parameter (https://your_domain/context_path+";JSESSIONID=<new sessionId>"). But when this URL hits the Controller endpoint, Spring will reject the request as it identifies a malicious string (the semicolon ";") in the URL.
  2. Based on the factor that you are having a web application which required user logins at the first place and 3rd party URLs are allowed through security configuration without authentication and other factors such as HttpSecurity or WebSecurity, CSRF support, CORS support, etc…, the request might be rejected and redirect back to authentication.

in both of the cases what might happen is end user may have charged for the items from their bank account but those items might not marked for delivery from your inventory.

How to diagnose the issue in your site?

Google Chrome

Google chrome as gradually releasing this changes since FEB 2020 with stable version 80.0 and they have conformed that the change is 100% rolled out to all global users with the latest versions(>85). To make sure your browser is having the change, please follow the steps below (which is deprecated since v91).

  1. Goto chrome://flags/ and search for the following experiment properties and set the drop down value to Enabled.
    1. SameSite by default cookies
    2. Cookies without SameSite must be secure
SameSite experiment properties in Chrome.

UPDATE (14/07/2021) : Google Chrome has rolled out new SameSite cookie policy to all its browser users from stable version 80 and above and it supposed to be gradually increase the actual users with new behaviour. And additional note on Mar 18, 2021 said that the above two experimental flags were removed from Chrome 91 onward since the behaviour is enabled by default.

2. Relaunch the browser and check the Issues section in the Chrome Dev Tools. if following message is shown listing your session cookie under it, then your site might be affected.

SameSite warning in Google Chrome’s issue section in browser dev tools

Mozilla FireFox

At the time of writing, the latest stable version of the Mozilla Firefox is 82.0 and it doesn’t support SameSite attributes. But it will add support sooner with their upcoming releases as they are giving the option to check them in advance and prepare for the change.

  1. Go to about:config and accept the security risk warning.
  2. search for “network.cookie.sameSite.laxByDefault” and “network.cookie.sameSite.noneRequiresSecure” and toggle the values to true
SameSite Preference in Firefox

3. Restart the browser and check for the behavior. if your site is not properly configured with Samesite cookie attribute, a warning message will be shown in the browser dev tools like follows.

SameSite warning in dev tools section in Mozilla Firefox

You can check the compatibility of the change with rest of the browsers by visiting following links.

  1. Browser support for default SameSite value to Lax
  2. Browser support for SameSite=None
  3. Browser support for Secure context

Solution

As I’m having a Java background and practiced with Java I’ll put here some solutions with Spring boot and Spring security. you ‘ll able to find solutions for several other languages in here.

Note:
  1. If you are using the latest and updated version of the Spring and Spring security then the SameSite support is added with the framework itself. So first try with the inbuilt support and there are plenty of resources which implements CookieProcessors, manipulating the session cookie and so on. (in StackOverFlow specially)
  2. The best option will be to upgrade the Spring version to the latest in order to get the latest fixes and support for security features but we all know that is a tedious and time consuming process regarding to Development, testing and DevOps, with the complex nature of architectures and dependencies in enterprise applications. If you are in such a condition then you can find some time by trying on the following solutions until you do a proper framework upgrade.
source : giphy.com

Case 1 : Every single user need to be authenticated and rely on Spring security.

In this case, you might be added the security rules through a configuration class by extending the WebSecurityConfigurerAdapter class. You might have added rules through HttpHeaders, authorizations, sessionManagment and so on in there.

In here we are creating a servlet request filter by extending the GenericFilterBean class. What we do here is intercepting each servlet request which is not a request to fetch a resource and add the SameSite=None and Secure attributes to the JSESSIONID cookie by setting them through Set-Cookie response header.

import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

public class SessionCookieFilter extends GenericFilterBean {

    private final List<String> PATHS_TO_IGNORE_SETTING_SAMESITE = Arrays.asList("resources", <add other paths you want to exclude>);
    private final String SESSION_COOKIE_NAME = "JSESSIONID";
    private final String SESSION_PATH_ATTRIBUTE = ";Path=";
    private final String ROOT_CONTEXT = "/";
    private final String SAME_SITE_ATTRIBUTE_VALUES = ";HttpOnly;Secure;SameSite=None";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String requestUrl = req.getRequestURL().toString();
        boolean isResourceRequest = requestUrl != null ? StringUtils.isNoneBlank(PATHS_TO_IGNORE_SETTING_SAMESITE.stream().filter(s -> requestUrl.contains(s)).findFirst().orElse(null)) : null;
        if (!isResourceRequest) {
            Cookie[] cookies = ((HttpServletRequest) request).getCookies();
            if (cookies != null && cookies.length > 0) {
                List<Cookie> cookieList = Arrays.asList(cookies);
                Cookie sessionCookie = cookieList.stream().filter(cookie -> SESSION_COOKIE_NAME.equals(cookie.getName())).findFirst().orElse(null);
                if (sessionCookie != null) {
                    String contextPath = request.getServletContext() != null && StringUtils.isNotBlank(request.getServletContext().getContextPath()) ? request.getServletContext().getContextPath() : ROOT_CONTEXT;
                    resp.setHeader(HttpHeaders.SET_COOKIE, sessionCookie.getName() + "=" + sessionCookie.getValue() + SESSION_PATH_ATTRIBUTE + contextPath + SAME_SITE_ATTRIBUTE_VALUES);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

UPDATE (06/07/2021) : In here pay special attention to how the Path attribute was set with session cookie. Basically, in almost all the ServletContainers / Web Application frameworks, the Path attribute of the web site’s session cookie will be defaults to the ContextPath of the ServletContext of the web application deployed in a ServletContainer (unless it is not configured for a different context path through configurations). If we didn’t set the Path attribute specifically, then either ServletContainer or Web Application framework will set the Path to a different value and a new session cookie will be created with SameSite attributes and the new Path with the same JSESSIONID.

Ex : if your web application’s landing page after login is something like https://YOUR_DOMAIN/catalog/search/number then the contextPath would be /catalog. If you didn’t specify the Path attribute with session then there will be two session cookies like,

NamevaluePathhttpOnlysecureSameSite
JSESSIONID12345678/catalogfalsefalseLax
JSESSIONID12345678/searchfalsetrueNone
Session cookie conflict if Path was not specified

Web Application will work smoothly even with these two cookies but to make our solution more solid, the correct contextPath was fetched from the request and it was set as the Path attribute along with sameSite attributes. So at the end, the same session cookie will be overwritten with new sameSite attributes with same JSESSIONID. if the contextPath is Empty, which means your servelts resides in the ROOT(“/”) context, so we need to set Path="/" in such cases.

Then this filter need to be added to the Spring security Filter chain through your Security configurations,

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
           ....
           .addFilterAfter(new SessionCookieFilter(), BasicAuthenticationFilter.class);
}

In order to determine where you need to place the new filter in Spring’s security filter chain, you can debug the Spring security filter chain easily and identify a proper location in the filter chain. Apart from the BasicAuthenticationFilter, after the SecurityContextPersistanceFilter would be an another ideal place.

If you see this cookie attributes are not set in some browsers (like IE or Safari) or the fix is not working, the reason is those browsers/ browser versions might not support for SameSite attribute.

in such a case, it was advised in here to add both new cookie with SameSite attribute and a legacy cookie with only Secure attribute expecting that the browsers which not support SameSite will set it incorrectly or fallback to legacy cookie. But I have tried it in Java (as per code segment in below) by adding the legacy cookie attributes with a additional Set-cookie response header. But it didn’t work and what browsers will do is they’ll always pickup the last Set-cookie header coming with response headers.

//adding double cookies won't fix the problem in incompatible browsers
if (sessionCookie != null) {
    resp.setHeader(HttpHeaders.SET_COOKIE, sessionCookie.getName() + "=" + sessionCookie.getValue() + SAME_SITE_ATTRIBUTE_VALUES);
    resp.AddHeader(HttpHeaders.SET_COOKIE, sessionCookie.getName() + "=" + sessionCookie.getValue() + ";Secure");
}

Then I was thinking of a way to filter out incompatible browser clients being setting the new SameSite attribute. So I thought to use a external library to parse browser client information from user-agent header rather than doing it on my own. I have done some research over web and found out these couple of libraries as best candidates based on reputation, number of contributors, popularity, usage and updates.

  1. browscap-java – this was my first choice due to pretty decent API, better maintained repo, open source and easy integration. But when I used that with my Session Filter, it has drastically increased the loading times of my application requests which may occured due to heavy network calls or API calls.
  2. yauaa – another good library but as it runs under a “Free quota” on Google AppEngine, it will be unavailable when the quote is exceeded.

So I have decided to parse the user-agent header on my own(browser-sniffing) as there are only couple bf browsers which I need to check. As I’m checking user-agent against couple of old browsers and assuming that SameSite support will be added in the future releases of those browsers, I have added following user agent checking method.

private static final String _I_PHONE_IOS_12 = "iPhone OS 12_";
    private static final String _I_PAD_IOS_12 = "iPad; CPU OS 12_";
    private static final String _MAC_OS_10_14 = " OS X 10_14_";
    private static final String _VERSION = "Version/";
    private static final String _SAFARI = "Safari";
    private static final String _EMBED_SAFARI = "(KHTML, like Gecko)";
    private static final String _CHROME = "Chrome/";
    private static final String _CHROMIUM = "Chromium/";
    private static final String _UC_BROWSER = "UCBrowser/";
    private static final String _ANDROID = "Android";

    /*
     * checks SameSite=None;Secure incompatible Browsers
     * https://www.chromium.org/updates/same-site/incompatible-clients
     */
    public static boolean isSameSiteInCompatibleClient(HttpServletRequest request) {
        String userAgent = request.getHeader("user-agent");
        if (StringUtils.isNotBlank(userAgent)) {
            boolean isIos12 = isIos12(userAgent), isMacOs1014 = isMacOs1014(userAgent), isChromeChromium51To66 = isChromeChromium51To66(userAgent), isUcBrowser = isUcBrowser(userAgent);
            //TODO : Added for testing purpose. remove before Prod release.
            LOG.info("*********************************************************************************");
            LOG.info("is iOS 12 = {}, is MacOs 10.14 = {}, is Chrome 51-66 = {}, is Android UC Browser = {}", isIos12, isMacOs1014, isChromeChromium51To66, isUcBrowser);
            LOG.info("*********************************************************************************");
            return isIos12 || isMacOs1014 || isChromeChromium51To66 || isUcBrowser;
        }
        return false;
    }

    private static boolean isIos12(String userAgent) {
        return StringUtils.contains(userAgent, _I_PHONE_IOS_12) || StringUtils.contains(userAgent, _I_PAD_IOS_12);
    }

    private static boolean isMacOs1014(String userAgent) {
        return StringUtils.contains(userAgent, _MAC_OS_10_14)
            && ((StringUtils.contains(userAgent, _VERSION) && StringUtils.contains(userAgent, _SAFARI))  //Safari on MacOS 10.14
            || StringUtils.contains(userAgent, _EMBED_SAFARI)); // Embedded browser on MacOS 10.14
    }

    private static boolean isChromeChromium51To66(String userAgent) {
        boolean isChrome = StringUtils.contains(userAgent, _CHROME), isChromium = StringUtils.contains(userAgent, _CHROMIUM);
        if (isChrome || isChromium) {
            int version = isChrome ? Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROME).substring(0, 2))
                : Integer.valueOf(StringUtils.substringAfter(userAgent, _CHROMIUM).substring(0, 2));
            return ((version >= 51) && (version <= 66));    //Chrome or Chromium V51-66
        }
        return false;
    }

    private static boolean isUcBrowser(String userAgent) {
        if (StringUtils.contains(userAgent, _UC_BROWSER) && StringUtils.contains(userAgent, _ANDROID)) {
            String[] version = StringUtils.splitByWholeSeparator(StringUtils.substringAfter(userAgent, _UC_BROWSER).substring(0, 7), ".");
            int major = Integer.valueOf(version[0]), minor = Integer.valueOf(version[1]), build = Integer.valueOf(version[2]);
            return ((major != 0) && ((major < 12) || (major == 12 && (minor < 13)) || (major == 12 && minor == 13 && (build < 2)))); //UC browser below v12.13.2 in android
        }
        return false;
    }

Tested above code with the user-agent header values which I get logged in server log using the old versions of the browsers in Browserstack. Add the userAgent checking as follows in above SessionCookieFilter class.

 if (!isResourceRequest && !UserAgentUtils.isSameSiteInCompatibleClient(req)) {
Note

This fix won’t work in your localhost environment as Secure attribute needs to be set only in a secured connection(https). So you won’t be able to test this solution in your local environment so test this first in a test environment with https connection and push it to production.

Case 2 : No user authentication.

Solution 1

You can add the same GenericFilterBean approach which is described in the scenario where user need to be authenticated.

Solution 2

This solution will work for applications where you are not heavily rely on Spring security for user authentication but you may have several configurations over there like allowing URL without user authentication and so on.

Prerequisite : the request URL path which the 3rd party service provider sending back to your application when the client is redirecting to your domain, must be allowed either through WebSecurity or HttpSecurity.

ex:

 @Override
 public void configure(WebSecurity web) throws Exception {
            web
               .ignoring()
               .antMatchers(<3rd_party_response_URL>);
}

In this kind of scenario, you might have created an endpoint for above 3rd_party_response_URL(specially when it is a POST request in here) and perform some logics over there. In the first place where you trying to access the HttpSession through Spring, it might not able to identify the session due to the SameSite cookie issue. In this point a new httpSession is created and the sessionId will be appended at the end of the URL with ";JSESSIONID=" as a query parameter. Then Spring it self will rejects this request classifying it as a request which is having a malicious string.

What you can do here is, instead of accessing the httpSession in the controller method in the firstplace where 3rd party request hits your application, do a temporary redirection (GET) through the browser client to a separate controller method and access the httpSession over there. As this redirection is happening through client, browser will pass the Session cookie even if the the SameSite attribute defaults to Lax with the session cookie.

UML sequence diagram for solution with temporary redirect
@Controller
public class ThirdPartyResponseController{

    @RequestMapping(value=3rd_party_response_URL, method=RequestMethod.POST)
    public void thirdPartyresponse(HttpServletRequest request, HttpServletResponse httpServletResponse){
        // your logic
        // and you can set any data as an session attribute which you want to access over the 2nd controller 
        request.getSession().setAttribute(<data>)
        try {
            httpServletResponse.sendRedirect(<redirect_URL>);
        } catch (IOException e) {
            // handle error
        }
    }

    @RequestMapping(value=redirect_URL, method=RequestMethod.GET)
    public String thirdPartyresponse(HttpServletRequest request,  HttpServletResponse httpServletResponse, Model model,  RedirectAttributes redirectAttributes, HttpSession session){
        // your logic
        return <to_view>;
    }

}

httpServletResponse.sendRedirect() will do a redirection through your browser. The benefit with this approach is that you can test this even in your localhost environment.

If this helps you or you have different view/ideas/solutions or suggestions, please add and share them in comment section below.

Thank you.

Read Further

  1. SameSite announcement by Chromium in their blog.
  2. Good explanation about SameSite change in Heroku blog.
  3. Why double cookies won’t work and browser sniffing will work.
  4. Update From mozilla on rolling out SameSite in Firefox.

Read, comment, Share …

Check this out to solve Session cookie issues in your site with SameSite cookies in Spring

2 thoughts on “JSESSION, New SameSite cookie policy in Google Chrome and Spring

Leave a comment