diff --git a/changelog.md b/changelog.md index 3f14102..6df2aab 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Handler/action annotation security now shares `processInvalidAccess()` with route middleware. Behavior is unchanged. +### Fixed + +- Settings you set on the `cbcsrf` module itself were silently overwritten by the `csrf` key of the cbsecurity settings, even when you never set that key, because cbsecurity copied its defaults over them. Now only the keys you explicitly set in `cbsecurity.csrf` are applied, and they win over the `cbcsrf` module settings. Every other key keeps the `cbcsrf` value. `cbsecurity.csrf` now shows the values that are in effect. + ## [3.8.0] - 2026-09-21 ### Changed diff --git a/models/CBSecurity.cfc b/models/CBSecurity.cfc index b8ed8ca..26ae053 100644 --- a/models/CBSecurity.cfc +++ b/models/CBSecurity.cfc @@ -157,6 +157,12 @@ component threadsafe singleton accessors="true" { } function onDIComplete(){ + // What the user explicitly set in cbsecurity.csrf, captured before the defaults fill the gaps + var explicitCsrf = {}; + if ( structKeyExists( variables.settings, "csrf" ) && isStruct( variables.settings.csrf ) ) { + explicitCsrf = duplicate( variables.settings.csrf ); + } + // Default level-1 settings variables.settings.append( variables.DEFAULT_SETTINGS, false ); // Default level-2 settings @@ -197,14 +203,42 @@ component threadsafe singleton accessors="true" { log.info( "+ Basic Auth Validator Detected -> User Service set to BasicAuthUserService" ); } - // cbcsrf settings incorporation - variables.moduleSettings.cbcsrf.settings.append( variables.settings.csrf, true ); + // cbcsrf settings incorporation. Precedence, highest first: + // 1. Settings explicitly set in cbsecurity.csrf + // 2. Settings of the cbcsrf module (its own overrides or its defaults) + var cbcsrfSettings = variables.moduleSettings.cbcsrf.settings; + cbcsrfSettings.append( + resolveCsrfSettings( cbsecurityCsrf = explicitCsrf, cbcsrfSettings = cbcsrfSettings ), + true + ); + // Make cbsecurity.csrf show the values that are really in effect + for ( var csrfKey in variables.DEFAULT_SETTINGS.csrf ) { + if ( structKeyExists( cbcsrfSettings, csrfKey ) ) { + variables.settings.csrf[ csrfKey ] = cbcsrfSettings[ csrfKey ]; + } + } // DBLogger Configuration variables.dbLogger.configure(); // Log it log.info( "√ CBSecurity Services started and configured." ); } + /** + * Work out the settings for the cbcsrf module. The keys the user explicitly set in cbsecurity.csrf win, every + * other key keeps the value the cbcsrf module already has, so its own overrides are never replaced by + * cbsecurity defaults. + * + * @cbsecurityCsrf The keys the user explicitly set in cbsecurity.csrf + * @cbcsrfSettings The current settings of the cbcsrf module + * + * @return The cbcsrf settings to use + */ + struct function resolveCsrfSettings( required struct cbsecurityCsrf, required struct cbcsrfSettings ){ + var resolved = duplicate( arguments.cbcsrfSettings ); + resolved.append( arguments.cbsecurityCsrf, true ); + return resolved; + } + /** * Get the default rule settings structure */ diff --git a/test-harness/tests/specs/unit/CsrfSettingsSpec.cfc b/test-harness/tests/specs/unit/CsrfSettingsSpec.cfc new file mode 100644 index 0000000..6ace689 --- /dev/null +++ b/test-harness/tests/specs/unit/CsrfSettingsSpec.cfc @@ -0,0 +1,80 @@ +/** + * Which settings win between the cbsecurity `csrf` key and the cbcsrf module settings + */ +component extends="coldbox.system.testing.BaseModelTest" model="cbsecurity.models.CBSecurity" { + + function beforeAll(){ + super.beforeAll(); + // The defaults that both modules share + variables.defaults = { + enableAutoVerifier : false, + verifyExcludes : [], + rotationTimeout : 30, + enableEndpoint : false, + cacheStorage : "CacheStorage@cbstorages", + enableAuthTokenRotator : true + }; + } + + function run(){ + describe( "cbcsrf settings precedence", function(){ + beforeEach( function(){ + setup(); + } ); + + it( "leaves the cbcsrf settings alone when cbsecurity.csrf sets nothing", function(){ + var cbcsrf = duplicate( defaults ); + cbcsrf.rotationTimeout = 99; + cbcsrf.enableEndpoint = true; + + var resolved = model.resolveCsrfSettings( {}, cbcsrf ); + + expect( resolved.rotationTimeout ).toBe( 99 ); + expect( resolved.enableEndpoint ).toBeTrue(); + } ); + + it( "applies the keys the user set in cbsecurity.csrf over the defaults", function(){ + var resolved = model.resolveCsrfSettings( { rotationTimeout : 77 }, duplicate( defaults ) ); + + expect( resolved.rotationTimeout ).toBe( 77 ); + expect( resolved.enableEndpoint ).toBeFalse(); + } ); + + it( "lets an explicit cbsecurity.csrf key win over a cbcsrf override", function(){ + var cbcsrf = duplicate( defaults ); + cbcsrf.rotationTimeout = 99; + + var resolved = model.resolveCsrfSettings( { rotationTimeout : 77 }, cbcsrf ); + + expect( resolved.rotationTimeout ).toBe( 77 ); + } ); + + it( "keeps cbcsrf overrides for keys cbsecurity did not set", function(){ + var cbcsrf = duplicate( defaults ); + cbcsrf.rotationTimeout = 99; + cbcsrf.enableEndpoint = true; + + var resolved = model.resolveCsrfSettings( { rotationTimeout : 77 }, cbcsrf ); + + expect( resolved.rotationTimeout ).toBe( 77 ); + expect( resolved.enableEndpoint ).toBeTrue(); + } ); + + it( "applies complex values from cbsecurity.csrf", function(){ + var cbcsrf = duplicate( defaults ); + cbcsrf.verifyExcludes = [ "stripe" ]; + + var resolved = model.resolveCsrfSettings( { verifyExcludes : [ "other" ] }, cbcsrf ); + + expect( resolved.verifyExcludes ).toBe( [ "other" ] ); + } ); + + it( "does not change the settings it is given", function(){ + var cbcsrf = duplicate( defaults ); + model.resolveCsrfSettings( { rotationTimeout : 77 }, cbcsrf ); + expect( cbcsrf.rotationTimeout ).toBe( 30 ); + } ); + } ); + } + +}