From 99a5040a2af583f345ae69851f85c6368d2a4f89 Mon Sep 17 00:00:00 2001 From: Grant Copley Date: Sat, 22 Nov 2025 16:03:07 -0600 Subject: [PATCH 01/10] Change CSRF implementation to default to using SessionStorage instead of CacheStorage to remove 'page expired' errors and make extensible. --- .claude/settings.local.json | 12 +++ ModuleConfig.cfc | 22 ++++- box.json | 4 +- models/CBWIREController.cfc | 25 +++--- models/interfaces/ICSRFStorage.cfc | 49 ++++++++++ models/services/TokenService.cfc | 89 +++++++++++++++++++ models/services/csrf/CacheCSRFStorage.cfc | 70 +++++++++++++++ models/services/csrf/SessionCSRFStorage.cfc | 69 ++++++++++++++ test-harness/layouts/Main.cfm | 2 +- .../specs/unit/services/TokenServiceSpec.cfc | 81 +++++++++++++++++ .../services/csrf/CacheCSRFStorageSpec.cfc | 54 +++++++++++ .../services/csrf/SessionCSRFStorageSpec.cfc | 54 +++++++++++ 12 files changed, 515 insertions(+), 16 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 models/interfaces/ICSRFStorage.cfc create mode 100644 models/services/TokenService.cfc create mode 100644 models/services/csrf/CacheCSRFStorage.cfc create mode 100644 models/services/csrf/SessionCSRFStorage.cfc create mode 100644 test-harness/tests/specs/unit/services/TokenServiceSpec.cfc create mode 100644 test-harness/tests/specs/unit/services/csrf/CacheCSRFStorageSpec.cfc create mode 100644 test-harness/tests/specs/unit/services/csrf/SessionCSRFStorageSpec.cfc diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..51f06982 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(box testbox run:*)", + "WebSearch", + "WebFetch(domain:github.com)" + ], + "deny": [], + "ask": [] + } +} diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc index f6738036..44e198c7 100644 --- a/ModuleConfig.cfc +++ b/ModuleConfig.cfc @@ -3,7 +3,7 @@ component { this.version = "@build.version@+@build.number@"; this.author = "Ortus Solutions"; this.webUrl = "https://github.com/coldbox-modules/cbwire"; - this.dependencies = []; + this.dependencies = [ "cbstorages" ]; this.entryPoint = "cbwire"; this.layoutParentLookup = false; this.viewParentLookup = false; @@ -66,7 +66,25 @@ component { * We recommend always leaving this enabled, but you can disable it * as needed. */ - "checksumValidation": true + "checksumValidation": true, + /** + * Enables Cross-Site Request Forgery (CSRF) protection for CBWIRE requests. + * When enabled, all component actions require a valid CSRF token. + * When disabled, checksum validation still provides security against tampering. + */ + "csrfEnabled": true, + /** + * Specifies the WireBox mapping for the CSRF token storage service. + * The service must implement the ICSRFStorage interface. + * + * Built-in options: + * - "SessionCSRFStorage@cbwire" (default) - Session-based storage, OWASP recommended + * - "CacheCSRFStorage@cbwire" - Cache-based storage for distributed/clustered systems + * + * You can also provide your own custom implementation that implements + * cbwire.models.interfaces.ICSRFStorage + */ + "csrfService": "SessionCSRFStorage@cbwire" }; routes = [ diff --git a/box.json b/box.json index 94856d5f..902f2204 100644 --- a/box.json +++ b/box.json @@ -22,7 +22,7 @@ ], "contributors":[], "dependencies":{ - "cbcsrf":"^3.1.0+16" + "cbstorages":"^3.0.0" }, "devDependencies":{ "commandbox-cfformat":"*", @@ -45,6 +45,6 @@ "runner":"http://localhost:60299/tests/runner.cfm" }, "installPaths":{ - "cbcsrf":"modules/cbcsrf/" + "cbstorages":"modules/cbstorages/" } } \ No newline at end of file diff --git a/models/CBWIREController.cfc b/models/CBWIREController.cfc index 6b678ee5..1724d321 100644 --- a/models/CBWIREController.cfc +++ b/models/CBWIREController.cfc @@ -6,8 +6,8 @@ component accessors="true" singleton { // Injected RequestService so that we can access the current ColdBox RequestContext. property name="requestService" inject="coldbox:requestService"; - // Inject CBCSRF for CSRF token generation and verification - property name="cbcsrf" inject="provider:@cbcsrf"; + // Inject TokenService for CSRF token generation and verification + property name="tokenService" inject="provider:TokenService@cbwire"; // Inject module settings property name="moduleSettings" inject="coldbox:modulesettings:cbwire"; @@ -84,13 +84,13 @@ component accessors="true" singleton { } ); - // Set the CSRF token for the request - local.csrfToken = local.payload._token; - // Validate the CSRF token - local.csrfTokenVerified = variables.wirebox.getInstance( dsl="@cbcsrf" ).verify( local.csrfToken ); - // Check the CSRF token, throw 403 if invalid - if( !local.csrfTokenVerified ){ - throw( type="CBWIREException", message="Page expired." ); + // Verify CSRF token if CSRF protection is enabled + if ( variables.moduleSettings.csrfEnabled ) { + local.csrfToken = local.payload._token; + local.csrfTokenVerified = variables.tokenService.verify( local.csrfToken ); + if( !local.csrfTokenVerified ){ + throw( type="CBWIREException", message="Page expired." ); + } } // Perform additional deserialization of the component snapshots @@ -530,12 +530,15 @@ component accessors="true" singleton { /** * Generates a CSRF token for the current request. + * Returns empty string if CSRF protection is disabled. * * @return string */ function generateCSRFToken() { - // Generate the CSRF token using the cbcsrf library - return variables.cbcsrf.generate(); + // Generate the CSRF token using cbwire's token service if enabled + return variables.moduleSettings.csrfEnabled ? + variables.tokenService.generate() : + ""; } /** diff --git a/models/interfaces/ICSRFStorage.cfc b/models/interfaces/ICSRFStorage.cfc new file mode 100644 index 00000000..354a2526 --- /dev/null +++ b/models/interfaces/ICSRFStorage.cfc @@ -0,0 +1,49 @@ +/** + * Interface for CSRF token storage implementations in cbwire. + * + * Implementations must provide thread-safe storage and retrieval + * of CSRF tokens with session-lifetime persistence. + * + * @author Ortus Solutions + */ +interface { + + /** + * Stores a CSRF token for the current session/user context. + * + * @token The CSRF token to store + * + * @return The storage implementation instance (for method chaining) + */ + any function set( required string token ); + + /** + * Retrieves the stored CSRF token for the current session/user context. + * + * @return The stored token, or empty string if no token exists + */ + string function get(); + + /** + * Checks if a CSRF token exists in storage for the current session/user context. + * + * @return True if a token exists, false otherwise + */ + boolean function exists(); + + /** + * Removes the CSRF token from storage (used for rotation/logout). + * + * @return The storage implementation instance (for method chaining) + */ + any function delete(); + + /** + * Clears all CSRF-related data from storage. + * Useful for cleanup or testing scenarios. + * + * @return The storage implementation instance (for method chaining) + */ + any function clear(); + +} diff --git a/models/services/TokenService.cfc b/models/services/TokenService.cfc new file mode 100644 index 00000000..ba032e1f --- /dev/null +++ b/models/services/TokenService.cfc @@ -0,0 +1,89 @@ +/** + * CBWIRE-specific token service for preventing CSRF attacks + * without interfering with cbcsrf module settings. + * + * Delegates storage operations to a configurable ICSRFStorage implementation. + * + * @author Ortus Solutions + */ +component accessors="true" singleton { + + property name="moduleSettings" inject="coldbox:modulesettings:cbwire"; + property name="wirebox" inject="wirebox"; + + /** + * Lazy-loaded CSRF storage service + */ + variables.csrfService = ""; + + /** + * Generates a CBWIRE-specific token that doesn't expire. + * Stored using the configured storage implementation and lasts for session lifetime. + * + * @return The generated or existing token + */ + function generate() { + // If we don't have a token yet, generate one + if ( !getCSRFService().exists() ) { + var newToken = generateNewToken(); + getCSRFService().set( newToken ); + } + + return getCSRFService().get(); + } + + /** + * Verifies a CBWIRE token + * + * @token The token to verify + * + * @return True if valid, false otherwise + */ + function verify( required string token ) { + // Verify token exists and matches + return getCSRFService().exists() && getCSRFService().get() == arguments.token; + } + + /** + * Rotates the token (for logout, etc) + * + * @return TokenService instance for chaining + */ + function rotate() { + getCSRFService().delete(); + return this; + } + + // Private methods + + /** + * Lazy-loads the CSRF storage service based on module settings + * + * @return The configured ICSRFStorage implementation + */ + private function getCSRFService() { + if ( !isObject( variables.csrfService ) ) { + variables.csrfService = wirebox.getInstance( moduleSettings.csrfService ); + } + return variables.csrfService; + } + + private function generateNewToken() { + // Generate a cryptographically secure random token + var tokenBase = "#createUUID()##getRealIP()##randRange( 0, 65535, "SHA1PRNG" )##getTickCount()#"; + return uCase( left( hash( tokenBase & session.sessionid, "SHA-256" ), 40 ) ); + } + + private function getRealIP() { + var headers = getHTTPRequestData().headers; + + if ( structKeyExists( headers, "x-cluster-client-ip" ) ) { + return headers[ "x-cluster-client-ip" ]; + } + if ( structKeyExists( headers, "X-Forwarded-For" ) ) { + return headers[ "X-Forwarded-For" ]; + } + + return len( CGI.REMOTE_ADDR ) ? CGI.REMOTE_ADDR : "127.0.0.1"; + } +} diff --git a/models/services/csrf/CacheCSRFStorage.cfc b/models/services/csrf/CacheCSRFStorage.cfc new file mode 100644 index 00000000..9232730a --- /dev/null +++ b/models/services/csrf/CacheCSRFStorage.cfc @@ -0,0 +1,70 @@ +/** + * Cache-based CSRF token storage using cbstorages cacheStorage. + * Suitable for distributed/clustered environments where session replication + * is not available or desired. + * + * Tokens are stored in the configured CacheBox cache provider. + * Does not require session management to be enabled - uses cookie/URL fallback. + * + * @author Ortus Solutions + */ +component + accessors="true" + singleton + implements="cbwire.models.interfaces.ICSRFStorage" +{ + + property name="cacheStorage" inject="cacheStorage@cbstorages"; + + /** + * The storage key used for cbwire tokens + */ + variables.STORAGE_KEY = "_cbwire_token"; + + /** + * Stores a CSRF token in cache storage + * + * @token The CSRF token to store + */ + function set( required string token ) { + cacheStorage.set( + variables.STORAGE_KEY, + { "token": arguments.token, "createdAt": now() } + ); + return this; + } + + /** + * Retrieves the stored CSRF token from cache + * + * @return The stored token, or empty string if none exists + */ + string function get() { + var data = cacheStorage.get( variables.STORAGE_KEY, {} ); + return data.keyExists( "token" ) ? data.token : ""; + } + + /** + * Checks if a token exists in cache storage + */ + boolean function exists() { + return cacheStorage.exists( variables.STORAGE_KEY ) && + len( get() ) > 0; + } + + /** + * Removes the token from cache storage + */ + function delete() { + cacheStorage.delete( variables.STORAGE_KEY ); + return this; + } + + /** + * Clears all CSRF data (same as delete for cache storage) + */ + function clear() { + return delete(); + } + +} diff --git a/models/services/csrf/SessionCSRFStorage.cfc b/models/services/csrf/SessionCSRFStorage.cfc new file mode 100644 index 00000000..00370269 --- /dev/null +++ b/models/services/csrf/SessionCSRFStorage.cfc @@ -0,0 +1,69 @@ +/** + * Session-based CSRF token storage using cbstorages sessionStorage. + * This is the default storage mechanism for cbwire CSRF tokens. + * + * Tokens are stored in the session scope and persist for the session lifetime. + * Requires this.sessionManagement = true in Application.cfc. + * + * @author Ortus Solutions + */ +component + accessors="true" + singleton + implements="cbwire.models.interfaces.ICSRFStorage" +{ + + property name="sessionStorage" inject="sessionStorage@cbstorages"; + + /** + * The storage key used for cbwire tokens + */ + variables.STORAGE_KEY = "_cbwire_token"; + + /** + * Stores a CSRF token in session storage + * + * @token The CSRF token to store + */ + function set( required string token ) { + sessionStorage.set( + variables.STORAGE_KEY, + { "token": arguments.token, "createdAt": now() } + ); + return this; + } + + /** + * Retrieves the stored CSRF token from session + * + * @return The stored token, or empty string if none exists + */ + string function get() { + var data = sessionStorage.get( variables.STORAGE_KEY, {} ); + return data.keyExists( "token" ) ? data.token : ""; + } + + /** + * Checks if a token exists in session storage + */ + boolean function exists() { + return sessionStorage.exists( variables.STORAGE_KEY ) && + len( get() ) > 0; + } + + /** + * Removes the token from session storage + */ + function delete() { + sessionStorage.delete( variables.STORAGE_KEY ); + return this; + } + + /** + * Clears all CSRF data (same as delete for session storage) + */ + function clear() { + return delete(); + } + +} diff --git a/test-harness/layouts/Main.cfm b/test-harness/layouts/Main.cfm index e6302915..b6f5c043 100644 --- a/test-harness/layouts/Main.cfm +++ b/test-harness/layouts/Main.cfm @@ -6,7 +6,7 @@
-

Welcome to CBWIRE 4

+

Welcome to CBWIRE

#view()# diff --git a/test-harness/tests/specs/unit/services/TokenServiceSpec.cfc b/test-harness/tests/specs/unit/services/TokenServiceSpec.cfc new file mode 100644 index 00000000..99e6bc0a --- /dev/null +++ b/test-harness/tests/specs/unit/services/TokenServiceSpec.cfc @@ -0,0 +1,81 @@ +component extends="coldbox.system.testing.BaseTestCase" { + + function beforeAll() { + super.beforeAll(); + + // Get the token service from WireBox + variables.tokenService = getInstance( "TokenService@cbwire" ); + } + + function run() { + describe("TokenService", function() { + + beforeEach(function() { + // Clear any existing token before each test + tokenService.rotate(); + }); + + describe("generate()", function() { + + it("should generate a token", function() { + var token = tokenService.generate(); + + expect( token ).toBeString(); + expect( len( token ) ).toBeGT( 0 ); + }); + + it("should return same token on subsequent calls", function() { + var token1 = tokenService.generate(); + var token2 = tokenService.generate(); + + expect( token1 ).toBe( token2 ); + }); + }); + + describe("verify()", function() { + + it("should verify a valid token", function() { + var token = tokenService.generate(); + var isValid = tokenService.verify( token ); + + expect( isValid ).toBeTrue(); + }); + + it("should reject an invalid token", function() { + var isValid = tokenService.verify( "invalid-token-12345" ); + + expect( isValid ).toBeFalse(); + }); + + it("should reject empty token", function() { + var isValid = tokenService.verify( "" ); + + expect( isValid ).toBeFalse(); + }); + }); + + describe("rotate()", function() { + + it("should invalidate old token after rotation", function() { + var oldToken = tokenService.generate(); + + tokenService.rotate(); + + var isValid = tokenService.verify( oldToken ); + expect( isValid ).toBeFalse(); + }); + + it("should generate new token after rotation", function() { + var oldToken = tokenService.generate(); + + tokenService.rotate(); + + var newToken = tokenService.generate(); + + expect( newToken ).notToBe( oldToken ); + expect( tokenService.verify( newToken ) ).toBeTrue(); + }); + }); + }); + } +} diff --git a/test-harness/tests/specs/unit/services/csrf/CacheCSRFStorageSpec.cfc b/test-harness/tests/specs/unit/services/csrf/CacheCSRFStorageSpec.cfc new file mode 100644 index 00000000..acbdd2fe --- /dev/null +++ b/test-harness/tests/specs/unit/services/csrf/CacheCSRFStorageSpec.cfc @@ -0,0 +1,54 @@ +component extends="coldbox.system.testing.BaseTestCase" { + + function beforeAll() { + super.beforeAll(); + variables.storage = getInstance( "CacheCSRFStorage@cbwire" ); + } + + function run() { + describe("CacheCSRFStorage", function() { + + beforeEach(function() { + storage.clear(); + }); + + it("should store and retrieve a token", function() { + var token = "test-token-12345"; + storage.set( token ); + + expect( storage.get() ).toBe( token ); + }); + + it("should return empty string when no token exists", function() { + expect( storage.get() ).toBe( "" ); + }); + + it("should detect token existence", function() { + expect( storage.exists() ).toBeFalse(); + + storage.set( "token" ); + expect( storage.exists() ).toBeTrue(); + }); + + it("should delete token", function() { + storage.set( "token" ); + storage.delete(); + + expect( storage.exists() ).toBeFalse(); + }); + + it("should support method chaining", function() { + var result = storage.set( "token" ).delete(); + + expect( result ).toBe( storage ); + }); + + it("should clear token", function() { + storage.set( "token" ); + storage.clear(); + + expect( storage.exists() ).toBeFalse(); + }); + }); + } +} diff --git a/test-harness/tests/specs/unit/services/csrf/SessionCSRFStorageSpec.cfc b/test-harness/tests/specs/unit/services/csrf/SessionCSRFStorageSpec.cfc new file mode 100644 index 00000000..eb46bf8d --- /dev/null +++ b/test-harness/tests/specs/unit/services/csrf/SessionCSRFStorageSpec.cfc @@ -0,0 +1,54 @@ +component extends="coldbox.system.testing.BaseTestCase" { + + function beforeAll() { + super.beforeAll(); + variables.storage = getInstance( "SessionCSRFStorage@cbwire" ); + } + + function run() { + describe("SessionCSRFStorage", function() { + + beforeEach(function() { + storage.clear(); + }); + + it("should store and retrieve a token", function() { + var token = "test-token-12345"; + storage.set( token ); + + expect( storage.get() ).toBe( token ); + }); + + it("should return empty string when no token exists", function() { + expect( storage.get() ).toBe( "" ); + }); + + it("should detect token existence", function() { + expect( storage.exists() ).toBeFalse(); + + storage.set( "token" ); + expect( storage.exists() ).toBeTrue(); + }); + + it("should delete token", function() { + storage.set( "token" ); + storage.delete(); + + expect( storage.exists() ).toBeFalse(); + }); + + it("should support method chaining", function() { + var result = storage.set( "token" ).delete(); + + expect( result ).toBe( storage ); + }); + + it("should clear token", function() { + storage.set( "token" ); + storage.clear(); + + expect( storage.exists() ).toBeFalse(); + }); + }); + } +} From 7f1c63fe7f1171cfc06760a841148991e8851939 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:55:07 +0000 Subject: [PATCH 02/10] Initial plan From ac0d94a5c411c42a72302447388b4a878cdf7e8a Mon Sep 17 00:00:00 2001 From: Grant Copley Date: Tue, 2 Dec 2025 08:57:34 -0600 Subject: [PATCH 03/10] Update models/services/csrf/CacheCSRFStorage.cfc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- models/services/csrf/CacheCSRFStorage.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/services/csrf/CacheCSRFStorage.cfc b/models/services/csrf/CacheCSRFStorage.cfc index 9232730a..facf95c4 100644 --- a/models/services/csrf/CacheCSRFStorage.cfc +++ b/models/services/csrf/CacheCSRFStorage.cfc @@ -48,8 +48,8 @@ component * Checks if a token exists in cache storage */ boolean function exists() { - return cacheStorage.exists( variables.STORAGE_KEY ) && - len( get() ) > 0; + var data = cacheStorage.get( variables.STORAGE_KEY, {} ); + return data.keyExists( "token" ) && len( data.token ) > 0; } /** From 4608c4baeecab5d921ea162ac654f9b6159c5447 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:58:37 +0000 Subject: [PATCH 04/10] Add test coverage for csrfEnabled = false scenarios Co-authored-by: grantcopley <1197835+grantcopley@users.noreply.github.com> --- test-harness/tests/specs/CBWIRESpec.cfc | 46 +++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/test-harness/tests/specs/CBWIRESpec.cfc b/test-harness/tests/specs/CBWIRESpec.cfc index 764f3739..becc5c9e 100644 --- a/test-harness/tests/specs/CBWIRESpec.cfc +++ b/test-harness/tests/specs/CBWIRESpec.cfc @@ -707,6 +707,52 @@ component extends="coldbox.system.testing.BaseTestCase" { } ).toThrow( type="CBWIREException", message="Page expired." ); } ); + it( "should process requests without CSRF verification when csrfEnabled = false", function() { + var settings = getInstance( "coldbox:modulesettings:cbwire" ); + var originalSetting = settings.csrfEnabled; + settings.csrfEnabled = false; + + var payload = incomingRequest( + memo = { + "name": "TestComponent", + "id": "Z1Ruz1tGMPXSfw7osBW2", + "children": [] + }, + data = { + "count": 1 + }, + calls = [ + { + "path": "", + "method": "changeTitle", + "params": [] + } + ], + updates = {}, + csrfToken = "badToken" + ); + + // Should not throw an error even with bad token when CSRF is disabled + var response = cbwireController.handleRequest( payload, event ); + expect( isStruct( response ) ).toBeTrue(); + expect( response.components[1].effects.html ).toInclude( "CBWIRE Slays!" ); + + // Restore original setting + settings.csrfEnabled = originalSetting; + } ); + + it( "should return empty string from generateCSRFToken() when csrfEnabled = false", function() { + var settings = getInstance( "coldbox:modulesettings:cbwire" ); + var originalSetting = settings.csrfEnabled; + settings.csrfEnabled = false; + + var token = cbwireController.generateCSRFToken(); + expect( token ).toBe( "" ); + + // Restore original setting + settings.csrfEnabled = originalSetting; + } ); + it( "should provide a handleRequest() method that returns subsequent payloads", function() { var payload = incomingRequest( memo = { From 0f95972be8689b79d645b50b2ef94c6c7b556a34 Mon Sep 17 00:00:00 2001 From: Grant Copley Date: Tue, 2 Dec 2025 08:58:39 -0600 Subject: [PATCH 05/10] Update models/services/csrf/SessionCSRFStorage.cfc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- models/services/csrf/SessionCSRFStorage.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/services/csrf/SessionCSRFStorage.cfc b/models/services/csrf/SessionCSRFStorage.cfc index 00370269..c4848ed6 100644 --- a/models/services/csrf/SessionCSRFStorage.cfc +++ b/models/services/csrf/SessionCSRFStorage.cfc @@ -47,8 +47,8 @@ component * Checks if a token exists in session storage */ boolean function exists() { - return sessionStorage.exists( variables.STORAGE_KEY ) && - len( get() ) > 0; + var data = sessionStorage.get( variables.STORAGE_KEY, {} ); + return data.keyExists( "token" ) && len( data.token ) > 0; } /** From 3a377df1081ca5fc28b9d25289ebde75c45916f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:01:49 +0000 Subject: [PATCH 06/10] Initial plan From 04e21707864a40b9731f33cd211ac66ed4d05b84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:05:50 +0000 Subject: [PATCH 07/10] Use cross-platform session detection with getApplicationMetadata() Co-authored-by: grantcopley <1197835+grantcopley@users.noreply.github.com> --- models/services/TokenService.cfc | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/models/services/TokenService.cfc b/models/services/TokenService.cfc index ba032e1f..4de9a79c 100644 --- a/models/services/TokenService.cfc +++ b/models/services/TokenService.cfc @@ -71,7 +71,19 @@ component accessors="true" singleton { private function generateNewToken() { // Generate a cryptographically secure random token var tokenBase = "#createUUID()##getRealIP()##randRange( 0, 65535, "SHA1PRNG" )##getTickCount()#"; - return uCase( left( hash( tokenBase & session.sessionid, "SHA-256" ), 40 ) ); + + // Include session ID if sessions are enabled (cross-platform check) + var sessionId = ""; + var appSettings = getApplicationMetadata(); + if ( structKeyExists( appSettings, "sessionManagement" ) && appSettings.sessionManagement ) { + try { + sessionId = session.sessionid; + } catch ( any e ) { + // Session not available yet, continue without it + } + } + + return uCase( left( hash( tokenBase & sessionId, "SHA-256" ), 40 ) ); } private function getRealIP() { From f1b69f96c118501369f6d974b21cc609f19ff761 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:07:26 +0000 Subject: [PATCH 08/10] Cache app metadata and improve catch block comment Co-authored-by: grantcopley <1197835+grantcopley@users.noreply.github.com> --- models/services/TokenService.cfc | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/models/services/TokenService.cfc b/models/services/TokenService.cfc index 4de9a79c..3985b447 100644 --- a/models/services/TokenService.cfc +++ b/models/services/TokenService.cfc @@ -16,6 +16,11 @@ component accessors="true" singleton { */ variables.csrfService = ""; + /** + * Cache for application metadata to avoid repeated lookups + */ + variables.appMetadata = ""; + /** * Generates a CBWIRE-specific token that doesn't expire. * Stored using the configured storage implementation and lasts for session lifetime. @@ -74,18 +79,30 @@ component accessors="true" singleton { // Include session ID if sessions are enabled (cross-platform check) var sessionId = ""; - var appSettings = getApplicationMetadata(); - if ( structKeyExists( appSettings, "sessionManagement" ) && appSettings.sessionManagement ) { + if ( isSessionManagementEnabled() ) { try { sessionId = session.sessionid; } catch ( any e ) { - // Session not available yet, continue without it + // Handle cases where session scope exists but sessionid property is not yet available, + // or when session operations fail during application startup } } return uCase( left( hash( tokenBase & sessionId, "SHA-256" ), 40 ) ); } + /** + * Checks if session management is enabled in the application (cross-platform) + * + * @return True if session management is enabled, false otherwise + */ + private function isSessionManagementEnabled() { + if ( !isStruct( variables.appMetadata ) ) { + variables.appMetadata = getApplicationMetadata(); + } + return structKeyExists( variables.appMetadata, "sessionManagement" ) && variables.appMetadata.sessionManagement; + } + private function getRealIP() { var headers = getHTTPRequestData().headers; From a62978bb5902c466cca36fe462dd512b4d1dd77e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:08:44 +0000 Subject: [PATCH 09/10] Initialize appMetadata as struct and use structCount check Co-authored-by: grantcopley <1197835+grantcopley@users.noreply.github.com> --- models/services/TokenService.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/services/TokenService.cfc b/models/services/TokenService.cfc index 3985b447..003b29e9 100644 --- a/models/services/TokenService.cfc +++ b/models/services/TokenService.cfc @@ -19,7 +19,7 @@ component accessors="true" singleton { /** * Cache for application metadata to avoid repeated lookups */ - variables.appMetadata = ""; + variables.appMetadata = {}; /** * Generates a CBWIRE-specific token that doesn't expire. @@ -97,7 +97,7 @@ component accessors="true" singleton { * @return True if session management is enabled, false otherwise */ private function isSessionManagementEnabled() { - if ( !isStruct( variables.appMetadata ) ) { + if ( !structCount( variables.appMetadata ) ) { variables.appMetadata = getApplicationMetadata(); } return structKeyExists( variables.appMetadata, "sessionManagement" ) && variables.appMetadata.sessionManagement; From d297b39ed54956d43eab94957c9ce3fb84fcd29b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:09:47 +0000 Subject: [PATCH 10/10] Use structIsEmpty for better readability Co-authored-by: grantcopley <1197835+grantcopley@users.noreply.github.com> --- models/services/TokenService.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/services/TokenService.cfc b/models/services/TokenService.cfc index 003b29e9..55cb0c3b 100644 --- a/models/services/TokenService.cfc +++ b/models/services/TokenService.cfc @@ -97,7 +97,7 @@ component accessors="true" singleton { * @return True if session management is enabled, false otherwise */ private function isSessionManagementEnabled() { - if ( !structCount( variables.appMetadata ) ) { + if ( structIsEmpty( variables.appMetadata ) ) { variables.appMetadata = getApplicationMetadata(); } return structKeyExists( variables.appMetadata, "sessionManagement" ) && variables.appMetadata.sessionManagement;