Skip to content

Commit a97459b

Browse files
committed
LDEV-6046 sessionInvalidate() de-activate old session when using database storage
1 parent 1498f34 commit a97459b

File tree

13 files changed

+464
-11
lines changed

13 files changed

+464
-11
lines changed

core/src/main/java/lucee/runtime/type/scope/ScopeContext.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ public Session getExistingCFSessionScope(String applicationName, String cfid) {
504504
return null;
505505
}
506506

507-
public void removeSessionScope(PageContext pc) throws PageException {
507+
public void removeSessionScope(PageContext pc) {
508508
removeCFSessionScope(pc);
509509
removeJSessionScope(pc);
510510
}
@@ -517,24 +517,30 @@ public void removeJSessionScope(PageContext pc) {
517517
}
518518
}
519519

520-
public void removeCFSessionScope(PageContext pc) throws PageException {
520+
public void removeCFSessionScope(PageContext pc) {
521521

522522
ApplicationContext appContext = pc.getApplicationContext();
523523
Map<String, Scope> context = getSubMap(cfSessionContexts, appContext.getName());
524524
if (context != null) {
525-
context.remove(pc.getCFID());
526-
StorageScope scope = getCFScope(pc, false, Scope.SCOPE_SESSION);
527-
if (scope != null) scope.unstore(pc.getConfig());
525+
// LDEV-6046: Get scope BEFORE removing from memory, so we can unstore it
526+
// Don't use getCFScope() as it would reload from DB and put back in memory
527+
Scope scope = context.remove(pc.getCFID());
528+
if (scope instanceof StorageScope) {
529+
((StorageScope) scope).unstore(pc.getConfig());
530+
}
528531
}
529532
}
530533

531-
public void removeClientScope(PageContext pc) throws PageException {
534+
public void removeClientScope(PageContext pc) {
532535
ApplicationContext appContext = pc.getApplicationContext();
533536
Map<String, Scope> context = getSubMap(cfClientContexts, appContext.getName());
534537
if (context != null) {
535-
context.remove(pc.getCFID());
536-
StorageScope scope = getCFScope(pc, false, Scope.SCOPE_CLIENT);
537-
if (scope != null) scope.unstore(pc.getConfig());
538+
// LDEV-6046: Get scope BEFORE removing from memory, so we can unstore it
539+
// Don't use getCFScope() as it would reload from DB and put back in memory
540+
Scope scope = context.remove(pc.getCFID());
541+
if (scope instanceof StorageScope) {
542+
((StorageScope) scope).unstore(pc.getConfig());
543+
}
538544
}
539545
}
540546

loader/build.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<project default="core" basedir="." name="Lucee"
33
xmlns:resolver="antlib:org.apache.maven.resolver.ant">
44

5-
<property name="version" value="7.0.2.33-SNAPSHOT"/>
5+
<property name="version" value="7.0.2.34-SNAPSHOT"/>
66

77
<taskdef uri="antlib:org.apache.maven.resolver.ant" resource="org/apache/maven/resolver/ant/antlib.xml">
88
<classpath>

loader/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>org.lucee</groupId>
55
<artifactId>lucee</artifactId>
6-
<version>7.0.2.33-SNAPSHOT</version>
6+
<version>7.0.2.34-SNAPSHOT</version>
77
<packaging>jar</packaging>
88

99
<name>Lucee Loader Build</name>

test/tickets/LDEV6046.cfc

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
component extends="org.lucee.cfml.test.LuceeTestCase" labels="session,mysql" {
2+
3+
// LDEV-6046: sessionInvalidate() not de-activating old session when using DB for session storage
4+
// Reported: After v6.2.0.321, sessionInvalidate() stopped properly invalidating sessions stored in database
5+
//
6+
// The bug: sessionInvalidate() switches the user's cfid token but the old session remains active.
7+
// If a different browser uses the old cfid cookie value, the user is still logged in.
8+
// Works fine with sessionStorage = "memory", broken with database storage.
9+
10+
function isMySqlNotSupported() {
11+
var mySql = mySqlCredentials();
12+
return isEmpty( mysql );
13+
}
14+
15+
function run( testResults, testBox ) {
16+
describe( "LDEV-6046: sessionInvalidate() with database session storage", function() {
17+
18+
it( title="sessionInvalidate() should de-activate old session so it cannot be reused", skip=isMySqlNotSupported(), body=function( currentSpec ) {
19+
var uri = createURI( "LDEV6046" );
20+
21+
// First request - create session with user_id (simulating logged in user)
22+
var result1 = _InternalRequest(
23+
template: "#uri#/mysql/createSession.cfm"
24+
);
25+
var data1 = deserializeJSON( result1.filecontent.trim() );
26+
expect( data1.sessionId ).notToBeEmpty();
27+
expect( data1.user_id ).notToBeEmpty();
28+
29+
var originalSessionId = data1.sessionId;
30+
var originalCfid = data1.cfid;
31+
var originalCftoken = data1.cftoken;
32+
33+
// Second request - invalidate session (same "browser" - pass cookies)
34+
var result2 = _InternalRequest(
35+
template: "#uri#/mysql/invalidateSession.cfm",
36+
cookies: {
37+
cfid: originalCfid,
38+
cftoken: originalCftoken
39+
}
40+
);
41+
var data2 = deserializeJSON( result2.filecontent.trim() );
42+
43+
// Verify session was invalidated and a new one created
44+
expect( data2.oldSessionId ).toBe( originalSessionId );
45+
expect( data2.newSessionId ).notToBe( originalSessionId, "Session ID should change after sessionInvalidate()" );
46+
expect( data2.hasUserId ).toBeFalse( "New session should not have user_id from old session" );
47+
48+
// THE CRITICAL TEST: Simulate another browser trying to use the OLD cfid/cftoken
49+
// This should NOT return the old session with user_id
50+
var result3 = _InternalRequest(
51+
template: "#uri#/mysql/accessWithOldCfid.cfm",
52+
cookies: {
53+
cfid: originalCfid,
54+
cftoken: originalCftoken
55+
}
56+
);
57+
var data3 = deserializeJSON( result3.filecontent.trim() );
58+
59+
// The old session should NOT be accessible - user should not be "logged in"
60+
expect( data3.hasUserId ).toBeFalse( "Old session should not be accessible after sessionInvalidate() - user_id should not exist" );
61+
});
62+
63+
it( title="old session row should be deleted from database after sessionInvalidate()", skip=isMySqlNotSupported(), body=function( currentSpec ) {
64+
var uri = createURI( "LDEV6046" );
65+
66+
// Create session
67+
var result1 = _InternalRequest(
68+
template: "#uri#/mysql/createSession.cfm"
69+
);
70+
var data1 = deserializeJSON( result1.filecontent.trim() );
71+
var originalSessionId = data1.sessionId;
72+
var originalCfid = data1.cfid;
73+
var originalCftoken = data1.cftoken;
74+
75+
// Invalidate session (same browser - pass cookies)
76+
var result2 = _InternalRequest(
77+
template: "#uri#/mysql/invalidateSession.cfm",
78+
cookies: {
79+
cfid: originalCfid,
80+
cftoken: originalCftoken
81+
}
82+
);
83+
84+
// Verify old session row is removed from database
85+
var result3 = _InternalRequest(
86+
template: "#uri#/mysql/checkSession.cfm",
87+
url: { sessionId: originalSessionId }
88+
);
89+
var data3 = deserializeJSON( result3.filecontent.trim() );
90+
expect( data3.sessionExists ).toBeFalse( "Old session row should be deleted from cf_session_data table" );
91+
});
92+
93+
});
94+
95+
describe( "LDEV-6046: sessionInvalidate() with database session storage (sessionCluster=false)", function() {
96+
97+
it( title="sessionInvalidate() should de-activate old session with sessionCluster=false", skip=isMySqlNotSupported(), body=function( currentSpec ) {
98+
var uri = createURI( "LDEV6046" );
99+
100+
// First request - create session with user_id (simulating logged in user)
101+
var result1 = _InternalRequest(
102+
template: "#uri#/mysql/createSession.cfm",
103+
url: { sessionCluster: false }
104+
);
105+
var data1 = deserializeJSON( result1.filecontent.trim() );
106+
expect( data1.sessionId ).notToBeEmpty();
107+
expect( data1.user_id ).notToBeEmpty();
108+
109+
var originalSessionId = data1.sessionId;
110+
var originalCfid = data1.cfid;
111+
var originalCftoken = data1.cftoken;
112+
113+
// Second request - invalidate session (same "browser" - pass cookies)
114+
var result2 = _InternalRequest(
115+
template: "#uri#/mysql/invalidateSession.cfm",
116+
url: { sessionCluster: false },
117+
cookies: {
118+
cfid: originalCfid,
119+
cftoken: originalCftoken
120+
}
121+
);
122+
var data2 = deserializeJSON( result2.filecontent.trim() );
123+
124+
// Verify session was invalidated and a new one created
125+
expect( data2.oldSessionId ).toBe( originalSessionId );
126+
expect( data2.newSessionId ).notToBe( originalSessionId, "Session ID should change after sessionInvalidate()" );
127+
expect( data2.hasUserId ).toBeFalse( "New session should not have user_id from old session" );
128+
129+
// THE CRITICAL TEST: Simulate another browser trying to use the OLD cfid/cftoken
130+
var result3 = _InternalRequest(
131+
template: "#uri#/mysql/accessWithOldCfid.cfm",
132+
url: { sessionCluster: false },
133+
cookies: {
134+
cfid: originalCfid,
135+
cftoken: originalCftoken
136+
}
137+
);
138+
var data3 = deserializeJSON( result3.filecontent.trim() );
139+
140+
// The old session should NOT be accessible - user should not be "logged in"
141+
expect( data3.hasUserId ).toBeFalse( "Old session should not be accessible after sessionInvalidate() with sessionCluster=false" );
142+
});
143+
144+
it( title="old session row should be deleted from database after sessionInvalidate() with sessionCluster=false", skip=isMySqlNotSupported(), body=function( currentSpec ) {
145+
var uri = createURI( "LDEV6046" );
146+
147+
// Create session
148+
var result1 = _InternalRequest(
149+
template: "#uri#/mysql/createSession.cfm",
150+
url: { sessionCluster: false }
151+
);
152+
var data1 = deserializeJSON( result1.filecontent.trim() );
153+
var originalSessionId = data1.sessionId;
154+
var originalCfid = data1.cfid;
155+
var originalCftoken = data1.cftoken;
156+
157+
// Invalidate session (same browser - pass cookies)
158+
var result2 = _InternalRequest(
159+
template: "#uri#/mysql/invalidateSession.cfm",
160+
url: { sessionCluster: false },
161+
cookies: {
162+
cfid: originalCfid,
163+
cftoken: originalCftoken
164+
}
165+
);
166+
167+
// Verify old session row is removed from database
168+
var result3 = _InternalRequest(
169+
template: "#uri#/mysql/checkSession.cfm",
170+
url: { sessionId: originalSessionId, sessionCluster: false }
171+
);
172+
var data3 = deserializeJSON( result3.filecontent.trim() );
173+
expect( data3.sessionExists ).toBeFalse( "Old session row should be deleted from cf_session_data table with sessionCluster=false" );
174+
});
175+
176+
});
177+
178+
describe( "LDEV-6046: sessionInvalidate() with MEMORY session storage (control test)", function() {
179+
180+
it( title="sessionInvalidate() should de-activate old session with memory storage", body=function( currentSpec ) {
181+
var uri = createURI( "LDEV6046" );
182+
183+
// First request - create session with user_id (simulating logged in user)
184+
var result1 = _InternalRequest(
185+
template: "#uri#/memory/createSession.cfm"
186+
);
187+
var data1 = deserializeJSON( result1.filecontent.trim() );
188+
expect( data1.sessionId ).notToBeEmpty();
189+
expect( data1.user_id ).notToBeEmpty();
190+
191+
var originalSessionId = data1.sessionId;
192+
var originalCfid = data1.cfid;
193+
var originalCftoken = data1.cftoken;
194+
195+
// Second request - invalidate session (same "browser" - pass cookies)
196+
var result2 = _InternalRequest(
197+
template: "#uri#/memory/invalidateSession.cfm",
198+
cookies: {
199+
cfid: originalCfid,
200+
cftoken: originalCftoken
201+
}
202+
);
203+
var data2 = deserializeJSON( result2.filecontent.trim() );
204+
205+
// Verify session was invalidated and a new one created
206+
expect( data2.oldSessionId ).toBe( originalSessionId );
207+
expect( data2.newSessionId ).notToBe( originalSessionId, "Session ID should change after sessionInvalidate()" );
208+
expect( data2.hasUserId ).toBeFalse( "New session should not have user_id from old session" );
209+
210+
// THE CRITICAL TEST: Simulate another browser trying to use the OLD cfid/cftoken
211+
// With memory storage, this should work correctly
212+
var result3 = _InternalRequest(
213+
template: "#uri#/memory/accessWithOldCfid.cfm",
214+
cookies: {
215+
cfid: originalCfid,
216+
cftoken: originalCftoken
217+
}
218+
);
219+
var data3 = deserializeJSON( result3.filecontent.trim() );
220+
221+
// The old session should NOT be accessible - user should not be "logged in"
222+
expect( data3.hasUserId ).toBeFalse( "Old session should not be accessible after sessionInvalidate() - user_id should not exist (memory storage)" );
223+
});
224+
225+
});
226+
}
227+
228+
private string function createURI( string calledName ) {
229+
var baseURI = "/test/#listLast( getDirectoryFromPath( getCurrentTemplatePath() ), "\/" )#/";
230+
return baseURI & calledName;
231+
}
232+
233+
private struct function mySqlCredentials() {
234+
return server.getDatasource( "mysql" );
235+
}
236+
237+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
component {
2+
this.name = "ldev-6046-memory-" & hash( getCurrentTemplatePath() );
3+
this.sessionManagement = true;
4+
this.sessionTimeout = createTimeSpan( 0, 0, 5, 0 );
5+
this.setClientCookies = true;
6+
7+
// Use memory for session storage - this should work correctly
8+
this.sessionStorage = "memory";
9+
10+
public function onRequestStart() {
11+
setting requesttimeout=10 showdebugOutput=false;
12+
}
13+
14+
function onSessionStart() {
15+
systemOutput("------onSessionStart (memory)", true);
16+
session.started = now();
17+
}
18+
19+
function onSessionEnd( sessionScope, applicationScope ) {
20+
systemOutput("------onSessionEnd (memory)", true);
21+
server.LDEV6046_ended_sessions[ arguments.sessionScope.sessionid ] = now();
22+
}
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<cfscript>
2+
// This simulates another browser/request trying to access with the OLD cfid/cftoken
3+
// After sessionInvalidate(), this should NOT return a valid session with user_id
4+
5+
result = {
6+
sessionId: session.sessionid,
7+
hasUserId: structKeyExists( session, "user_id" ),
8+
userId: session.user_id ?: "",
9+
hasTestValue: structKeyExists( session, "testValue" ),
10+
testValue: session.testValue ?: ""
11+
};
12+
13+
echo( serializeJSON( result ) );
14+
</cfscript>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<cfscript>
2+
// Store test value if provided
3+
if ( structKeyExists( url, "testValue" ) ) {
4+
session.testValue = url.testValue;
5+
}
6+
7+
// Set a marker to prove session is active
8+
session.user_id = createUUID();
9+
10+
result = {
11+
sessionId: session.sessionid,
12+
cfid: session.cfid,
13+
cftoken: session.cftoken,
14+
user_id: session.user_id,
15+
testValue: session.testValue ?: ""
16+
};
17+
18+
echo( serializeJSON( result ) );
19+
</cfscript>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<cfscript>
2+
// Capture old session info before invalidation
3+
oldSessionId = session.sessionid;
4+
oldCfid = session.cfid;
5+
oldCftoken = session.cftoken;
6+
7+
// This is the function we're testing
8+
sessionInvalidate();
9+
10+
// After invalidation, we should have a NEW session
11+
result = {
12+
oldSessionId: oldSessionId,
13+
oldCfid: oldCfid,
14+
oldCftoken: oldCftoken,
15+
newSessionId: session.sessionid,
16+
newCfid: session.cfid,
17+
newCftoken: session.cftoken,
18+
hasTestValue: structKeyExists( session, "testValue" ),
19+
hasUserId: structKeyExists( session, "user_id" )
20+
};
21+
22+
echo( serializeJSON( result ) );
23+
</cfscript>

0 commit comments

Comments
 (0)