spring-projects / spring-session

Spring Session
https://spring.io/projects/spring-session
Apache License 2.0
1.86k stars 1.12k forks source link

Corrupted Session ID occasionally breaks my application (base64Encoding) #1201

Closed BenDol closed 5 years ago

BenDol commented 6 years ago

I am using the standard Spring Session set up and occasionally the httpSessionIdResolver is returning some form of corrupted session ID. Is there a way to determine if it is corrupted and avoid this? Or perhaps I have a bad configuration that is causing this to happen occasionally?

From my debugging the DefaultCookieSerializer is returning: �~6�9�P��

cookie = {Cookie@13809} 
 name = "BSESSION"
 value = "2342674501D2E98A7A5AFEE4073C5A16"
 comment = null
 domain = null
 maxAge = -1
 path = null
 secure = false
 version = 0
 isHttpOnly = false
sessionId = "�~6�9�P��

The exception I receive:

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [SELECT S.PRIMARY_ID, S.SESSION_ID, S.CREATION_TIME, S.LAST_ACCESS_TIME, S.MAX_INACTIVE_INTERVAL, SA.ATTRIBUTE_NAME, SA.ATTRIBUTE_BYTES FROM SPRING_SESSION S LEFT OUTER JOIN SPRING_SESSION_ATTRIBUTES SA ON S.PRIMARY_ID = SA.SESSION_PRIMARY_ID WHERE S.SESSION_ID = ?ERROR: invalid byte sequence for encoding "UTF8": 0x00; nested exception is org.postgresql.util.PSQLException: ERROR: invalid byte sequence for encoding "UTF8": 0x00
    at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:104)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
    at org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1402)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:620)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:657)
    at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:688)
    at org.springframework.session.jdbc.JdbcOperationsSessionRepository.lambda$findById$1(JdbcOperationsSessionRepository.java:428)
    at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140)
    at org.springframework.session.jdbc.JdbcOperationsSessionRepository.findById(JdbcOperationsSessionRepository.java:427)
    at org.springframework.session.jdbc.JdbcOperationsSessionRepository.findById(JdbcOperationsSessionRepository.java:115)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getRequestedSession(SessionRepositoryFilter.java:364)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:301)
    at org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper.getSession(SessionRepositoryFilter.java:197)
    at com.insclix.core.spring.session.AbstractSessionEnhancerFilter.doFilterInternal(AbstractSessionEnhancerFilter.java:25)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:147)
    at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:81)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:155)
    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:123)
    at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:108)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:478)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1455)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)
Caused by: org.postgresql.util.PSQLException: ERROR: invalid byte sequence for encoding "UTF8": 0x00
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2422)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2167)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:306)
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:441)
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:365)
    at org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:155)
    at org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:118)
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)
    at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:666)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:605)
    ... 43 common frames omitted

I'll continue debugging and update if I make any progress, but any help is appreciated.

BenDol commented 6 years ago

The corruption of the session could be due to some debug breakpoints I had where I stopped the application while development (before the cookie serializer had time to write the cookie?). But I am somewhat suspicious given the fact that the cookie value seems to be in a different format to springs.

EDIT: It happened again out of the blue, this time it was a fresh database, seems that the session ID is being set somewhere else perhaps?

BenDol commented 6 years ago

I suspect since I have added the property server.servlet.session.cookie.name it is trying to push the JSESSIONID into this session cookie as well as my actual session? I'm not really sure how that all works if it is the case. I removed that property and haven't had the issue since, I'll keep an eye out for it though.

EDIT: I think the issue is similar to this: https://github.com/spring-projects/spring-session/issues/607

vpavic commented 6 years ago

Thanks for the report @BenDol. Could you provide a minimal sample app that we could use to reproduce this?

cookie = {Cookie@13809} 
 name = "BSESSION"
 value = "2342674501D2E98A7A5AFEE4073C5A16"
 comment = null
 domain = null
 maxAge = -1
 path = null
 secure = false
 version = 0
 isHttpOnly = false
sessionId = "�~6�9�P��

Looking at this, it doesn't seem this is a cookie generated by Spring Session. What then happens is that DefaultCookieSerializer attempts to Base64 decode cookie value and gets garbled output.

BenDol commented 6 years ago

@vpavic I'll put together a sample asap and you are correct, the session is being created by tomcat I think. When I remove the server.servlet.session.cookie.name property from my properties file the issue goes away and the JSESSIONID cookie all of a sudden appears when I run the application. So it seems like it's trying to push the JSESSIONID to the renamed session cookie name and in turn causing this error.

vpavic commented 6 years ago

Closing due to lack of feedback. Please comment back if you can provide more details and we can re-open the issue.

That being said, I believe the problem here lies outside of Spring Session, and is ordering related similarly like with #607 that you linked.

vpavic commented 6 years ago

Reopening instead of duplicate issue #1257 - please try to provide a minimal sample app that we could use to reproduce this. I suspect there is some request that isn't mapped on to our SessionRepositoryFilter so containers session management kicks in. If that's the case, this should be a configuration error but we need to be sure.

BenDol commented 6 years ago

I will try set up a sample environment when I have more time, it would mean reversing an app that requires oauth2 servers etc.

In the mean time I've found scopedTarget.oauth2ClientContext is the bean that is invoked in the AbstractBeanFactory#getBean which calls RequestFacade#getSession upon creation. Seems that it goes to the ManagerBase#createSession rather than to the Spring managed session factory?

setId:349, StandardSession (org.apache.catalina.session)
createSession:665, ManagerBase (org.apache.catalina.session)
doGetSession:3043, Request (org.apache.catalina.connector)
getSession:2437, Request (org.apache.catalina.connector)
getSession:896, RequestFacade (org.apache.catalina.connector)
getSession:116, ServletRequestAttributes (org.springframework.web.context.request)
obtainSession:138, ServletRequestAttributes (org.springframework.web.context.request)
getSessionMutex:264, ServletRequestAttributes (org.springframework.web.context.request)
get:55, SessionScope (org.springframework.web.context.request)
doGetBean:350, AbstractBeanFactory (org.springframework.beans.factory.support)
getBean:199, AbstractBeanFactory (org.springframework.beans.factory.support)
getTarget:35, SimpleBeanTargetSource (org.springframework.aop.target)
invoke:193, JdkDynamicAopProxy (org.springframework.aop.framework)
getAccessToken:-1, $Proxy130 (com.sun.proxy)
getAccessToken:169, OAuth2RestTemplate (org.springframework.security.oauth2.client)
attemptAuthentication:105, OAuth2ClientAuthenticationProcessingFilter (org.springframework.security.oauth2.client.filter)
doFilter:212, AbstractAuthenticationProcessingFilter (org.springframework.security.web.authentication)

...
vpavic commented 5 years ago

Closing due to lack of feedback. Please comment back if you can provide more details and we can re-open the issue.

patmagee commented 3 years ago

@vpavic We encountered this issue as well. For us it manifested when we were migrating from using the canonical servlet session management to spring-session management.

This is related in a way to a previous issue reported around migrating to spring boot 2.0.0.RELEASE, however is distinct since there is no migration, just going from HttpSessions to spring sessions.

Analysis

Any session cookies that were in browser pre-migration resulted in a corrupt state requiring users to clear their browser in order to properly interact with our services. This was hard to replicate and I needed to mimic our production environment where we have a resource server (using spring sessions) redirecting a user to an authorization server (also using spring sessions) and then redirecting back, where both servers were migrated simultaneously to the new setup.

The root cause of this appears to be a bad interplay between how tomcat generates the session ids, and how the DefaultCookieSerializer behaves OOTB. Tomcat appears to use org.apache.catalina.util.StandardSessionIdGenerator for generating session Id's if no other mechanism is provided. This creates a random string which is padded in order to meet the length requirements for the sessionId. The string that it produces is naturally not a base64 string, nor is it safe to be treated as a base64 string.

Unfortunately, when you migrate to the spring-session library, the DefaultCookieSerializer is configured OOTB to treat session Ids as b64 strings (see here), decoding them prior to passing them back to the SessionRepository.

In certain circumstances, the session padded session id generated from the StandardSessionIdGenerator when decoded as a b64 string includes \0x0 characters (byte code 0). Java does not care about this and happily converts the value to a string. Unfortunately, if you are using the Jdbc session store (that is all that I have tested this with on postgres), the database you are interacting may only allow a certain range of values, of which \0x0 is not one of them. This leads to a DataIntegrityViolationException which is also reported above. You can see the jshell snippet below demonstrating this:

jshell> var JSESSIONID="DE1F28DE6708BCE479BCB136DB9E9CAA";
jshell> Base64.getDecoder().decode(JSESSIONID);
$8 ==> byte[24] { 12, 77, 69, -37, -64, -60, -21, -67, 60, 4, 33, 56, -17, -48, 66, 7, 93, -6, 12, 31, 68, -12, 32, 0 }
 // notice the last byte is 0... Remove the extra `A` at the end and you get:
jshell> var JSESSIONID ==> "DE1F28DE6708BCE479BCB136DB9E9CA"
jshell> Base64.getDecoder().decode(JSESSIONID)
$10 ==> byte[23] { 12, 77, 69, -37, -64, -60, -21, -67, 60, 4, 33, 56, -17, -48, 66, 7, 93, -6, 12, 31, 68, -12, 32 }

Quick Fix

All you need to do is disable the base64 session encoding in the DefaultCookieSerializer. You can either extend the class (however then you are responsible for configuring it yourself), or you can use the Customizer object as shown below:

    @Bean
    public DefaultCookieSerializerCustomizer customizeCookieSerializer(){
        return cookieSerializer -> {
            cookieSerializer.setUseBase64Encoding(false);
        };
    }

TLDR

When migrating from using servlet managed HttpSession to SpringSessions make sure you disable base64 encoding for your cookies, or else pre-existing cookies will result in a 500 error for end users