From 4c947f21e063901d6ef44f543c809090152af42a Mon Sep 17 00:00:00 2001 From: Shane O'Donovan Date: Fri, 24 Jul 2026 12:39:41 +0100 Subject: [PATCH] LDEV-6447 Correct session invalidation lifecycle Avoid create-if-missing HttpSession calls during invalidation, including after a committed response. Do not eagerly create replacement sessions for sessionInvalidate(); subsequent SESSION access creates one lazily. Preserve sessionRotate() migration behavior. --- .../java/lucee/runtime/PageContextImpl.java | 3 +- .../runtime/type/scope/ScopeContext.java | 23 ++++++---- test/tickets/LDEV4166.cfc | 19 -------- test/tickets/LDEV6447.cfc | 44 +++++++++++++++++++ .../LDEV6447/jee-session/Application.cfc | 9 ++++ ...nvalidate-after-commit-without-session.cfm | 20 +++++++++ .../jee-session/invalidate-after-commit.cfm | 27 ++++++++++++ .../jee-session/invalidate-before-commit.cfm | 36 +++++++++++++++ 8 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 test/tickets/LDEV6447.cfc create mode 100644 test/tickets/LDEV6447/jee-session/Application.cfc create mode 100644 test/tickets/LDEV6447/jee-session/invalidate-after-commit-without-session.cfm create mode 100644 test/tickets/LDEV6447/jee-session/invalidate-after-commit.cfm create mode 100644 test/tickets/LDEV6447/jee-session/invalidate-before-commit.cfm diff --git a/core/src/main/java/lucee/runtime/PageContextImpl.java b/core/src/main/java/lucee/runtime/PageContextImpl.java index d401313be83..204fa2be2bc 100644 --- a/core/src/main/java/lucee/runtime/PageContextImpl.java +++ b/core/src/main/java/lucee/runtime/PageContextImpl.java @@ -3998,7 +3998,8 @@ public ClassLoader getRPCClassLoader(boolean reload, JavaSettings customJS) thro public void resetSession() { if (this.session != null && this.session instanceof JSession) { - getSession().invalidate(); + HttpSession httpSession = getHttpServletRequest().getSession(false); + if (httpSession != null) httpSession.invalidate(); } this.session = null; } diff --git a/core/src/main/java/lucee/runtime/type/scope/ScopeContext.java b/core/src/main/java/lucee/runtime/type/scope/ScopeContext.java index 9ebafd472be..579405d4cb1 100755 --- a/core/src/main/java/lucee/runtime/type/scope/ScopeContext.java +++ b/core/src/main/java/lucee/runtime/type/scope/ScopeContext.java @@ -963,7 +963,7 @@ public void invalidateUserScope(PageContextImpl pc, boolean migrateSessionData, if (hasSessionManagement) { if (isJ2EESession) { // For J2EE sessions, try the HttpSession attribute first - HttpSession httpSession = pc.getSession(); + HttpSession httpSession = pc.getHttpServletRequest().getSession(false); if (httpSession != null) { Object session = httpSession.getAttribute(appContext.getName()); if (session instanceof JSession) { @@ -999,7 +999,7 @@ public void invalidateUserScope(PageContextImpl pc, boolean migrateSessionData, // For J2EE sessions, handle the servlet container's session (JSESSIONID) if (isJ2EESession && hasSessionManagement) { - HttpSession httpSession = pc.getSession(); + HttpSession httpSession = pc.getHttpServletRequest().getSession(false); if (httpSession != null) { if (migrateSessionData) { // sessionRotate: rotate to a new session ID but keep session alive @@ -1016,21 +1016,26 @@ public void invalidateUserScope(PageContextImpl pc, boolean migrateSessionData, // For J2EE sessionRotate with a real httpSession (Tomcat), don't reset session - we already called // changeSessionId() and want to keep the data // But for JSR-223 (where httpSession is null), we need to reset to create a new session - HttpSession httpSessionForReset = pc.getSession(); + HttpSession httpSessionForReset = pc.getHttpServletRequest().getSession(false); if (!(isJ2EESession && migrateSessionData && httpSessionForReset != null)) { pc.resetSession(); } pc.resetClient(); if (oldSession != null) { - UserScope newSession; - if (isJ2EESession) { - newSession = getSessionScope(pc); + if (migrateSessionData) { + UserScope newSession; + if (isJ2EESession) { + newSession = getSessionScope(pc); + } + else { + newSession = (UserScope) getCFScope(pc, true, Scope.SCOPE_SESSION); + } + migrate(pc, oldSession, newSession, migrateSessionData); } else { - newSession = (UserScope) getCFScope(pc, true, Scope.SCOPE_SESSION); + oldSession.clear(); } - migrate(pc, oldSession, newSession, migrateSessionData); } if (oldClient != null) migrate(pc, oldClient, (UserScope) getCFScope(pc, true, Scope.SCOPE_CLIENT), migrateClientData); @@ -1063,4 +1068,4 @@ else if (newScope instanceof JSession && oldScope instanceof JSession) { } } -} \ No newline at end of file +} diff --git a/test/tickets/LDEV4166.cfc b/test/tickets/LDEV4166.cfc index 65e3846e760..0e0df97a79b 100644 --- a/test/tickets/LDEV4166.cfc +++ b/test/tickets/LDEV4166.cfc @@ -22,16 +22,6 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="session" { var appName = listFirst( trim( cfmlSessionId.filecontent ), '-' ) & "-" ; - // allow session to expire - expect( getSessionCount( appName ) ).toBe( 1 ); - - sleep(1001); - admin - action="purgeExpiredSessions" - type="server" - password="#request.SERVERADMINPASSWORD#"; - //systemOutput(server.LDEV4166_ended_CFML_Sessions, true); - // let's check first that the session actually ended! expect( getSessionCount( appName ) ).toBe( 0 ); expect( structKeyExists( server.LDEV4166_ended_CFML_Sessions, trim( cfmlSessionId.filecontent ) ) ).toBeTrue(); }); @@ -46,15 +36,6 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="session" { var appName = listFirst( trim( j2eeSessionId.filecontent ), '-' ) & "-" ; - expect( getSessionCount( appName ) ).toBe( 1 ); - // allow session to expire - sleep(1001); - admin - action="purgeExpiredSessions" - type="server" - password="#request.SERVERADMINPASSWORD#"; - //systemOutput(server.LDEV4166_ended_JEE_Sessions, true); - // let's check first that the session actually ended! expect( getSessionCount( appName ) ).toBe( 0 ); expect( structKeyExists( server.LDEV4166_ended_JEE_Sessions, trim( j2eeSessionId.filecontent ) ) ).toBeTrue(); }); diff --git a/test/tickets/LDEV6447.cfc b/test/tickets/LDEV6447.cfc new file mode 100644 index 00000000000..3c765a9d245 --- /dev/null +++ b/test/tickets/LDEV6447.cfc @@ -0,0 +1,44 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="session" { + + function run( testResults, testBox ) { + describe( "LDEV-6447: sessionInvalidate() JEE session lifecycle", function() { + it( title="does not eagerly create a replacement JEE session", skip=isJsr223(), body=function( currentSpec ) { + var data = request( "invalidate-before-commit.cfm" ); + expect( data.success ).toBeTrue( data.stacktrace ?: "no stacktrace" ); + expect( data.oldSessionInvalidated ).toBeTrue( "The existing HttpSession should be invalidated" ); + expect( data.replacementSessionExists ).toBeFalse( "sessionInvalidate() should not eagerly create a replacement HttpSession" ); + expect( data.sessionCreatedAfterAccess ).toBeTrue( "Accessing SESSION should lazily create a new HttpSession" ); + expect( data.oldSessionId ).notToBe( data.newSessionId, "The lazily-created session should have a new ID" ); + }); + + it( title="invalidates an existing JEE session", skip=isJsr223(), body=function( currentSpec ) { + var data = request( "invalidate-after-commit.cfm" ); + expect( data.success ).toBeTrue( data.stacktrace ?: "no stacktrace" ); + expect( data.sessionInvalidated ).toBeTrue( "The existing HttpSession should be invalidated" ); + }); + + it( title="is a no-op when no JEE session exists", skip=isJsr223(), body=function( currentSpec ) { + var data = request( "invalidate-after-commit-without-session.cfm" ); + expect( data.success ).toBeTrue( data.stacktrace ?: "no stacktrace" ); + }); + + }); + } + + private boolean function isJsr223() { + return cgi.request_url == "http://localhost/index.cfm"; + } + + private struct function request( required string template ) { + var hostIdx = find( cgi.script_name, cgi.request_url ); + if ( hostIdx <= 0 ) { + throw "Failed to extract host from CGI values"; + } + + var result = ""; + http method="get" + url="#left( cgi.request_url, hostIdx - 1 )#/test/tickets/LDEV6447/jee-session/#template#" + result="result"; + return deserializeJSON( result.filecontent ); + } +} diff --git a/test/tickets/LDEV6447/jee-session/Application.cfc b/test/tickets/LDEV6447/jee-session/Application.cfc new file mode 100644 index 00000000000..be79d7437e1 --- /dev/null +++ b/test/tickets/LDEV6447/jee-session/Application.cfc @@ -0,0 +1,9 @@ +component { + this.name = "ldev6447_jee_session_invalidate"; + this.sessionManagement = true; + this.sessionStorage = "memory"; + this.sessionTimeout = createTimeSpan( 0, 0, 0, 30 ); + this.setClientCookies = true; + this.applicationTimeout = createTimeSpan( 0, 0, 1, 0 ); + this.sessionType = "jee"; +} diff --git a/test/tickets/LDEV6447/jee-session/invalidate-after-commit-without-session.cfm b/test/tickets/LDEV6447/jee-session/invalidate-after-commit-without-session.cfm new file mode 100644 index 00000000000..a36486708f9 --- /dev/null +++ b/test/tickets/LDEV6447/jee-session/invalidate-after-commit-without-session.cfm @@ -0,0 +1,20 @@ + + result = { + success: true, + stacktrace: "" + }; + + sessionInvalidate(); + content type="application/json"; + getPageContext().getResponse().flushBuffer(); + + try { + sessionInvalidate(); + } + catch ( any e ) { + result.success = false; + result.stacktrace = e.stacktrace; + } + + echo( serializeJSON( result ) ); + diff --git a/test/tickets/LDEV6447/jee-session/invalidate-after-commit.cfm b/test/tickets/LDEV6447/jee-session/invalidate-after-commit.cfm new file mode 100644 index 00000000000..6717963b701 --- /dev/null +++ b/test/tickets/LDEV6447/jee-session/invalidate-after-commit.cfm @@ -0,0 +1,27 @@ + + result = { + success: true, + stacktrace: "", + sessionInvalidated: false + }; + + httpSession = getPageContext().getSession(); + content type="application/json"; + getPageContext().getResponse().flushBuffer(); + + try { + sessionInvalidate(); + try { + httpSession.getAttribute( "test" ); + } + catch ( any e ) { + result.sessionInvalidated = true; + } + } + catch ( any e ) { + result.success = false; + result.stacktrace = e.stacktrace; + } + + echo( serializeJSON( result ) ); + diff --git a/test/tickets/LDEV6447/jee-session/invalidate-before-commit.cfm b/test/tickets/LDEV6447/jee-session/invalidate-before-commit.cfm new file mode 100644 index 00000000000..f104a1951ca --- /dev/null +++ b/test/tickets/LDEV6447/jee-session/invalidate-before-commit.cfm @@ -0,0 +1,36 @@ + + result = { + success: true, + stacktrace: "", + oldSessionInvalidated: false, + replacementSessionExists: true, + sessionCreatedAfterAccess: false, + oldSessionId: "", + newSessionId: "" + }; + + try { + httpSession = getPageContext().getSession(); + result.oldSessionId = httpSession.getId(); + + sessionInvalidate(); + result.replacementSessionExists = !isNull( getPageContext().getRequest().getSession( false ) ); + + try { + httpSession.getAttribute( "test" ); + } + catch ( any e ) { + result.oldSessionInvalidated = true; + } + + result.newSessionId = session.sessionid; + result.sessionCreatedAfterAccess = !isNull( getPageContext().getRequest().getSession( false ) ); + } + catch ( any e ) { + result.success = false; + result.stacktrace = e.stacktrace; + } + + content type="application/json"; + echo( serializeJSON( result ) ); +