@@ -24,7 +24,7 @@ const { REDIS_INTEGRATION_MODE } = redis;
2424const { Logger } = require ( "./logger" ) ;
2525const { isOnCF, cfEnv } = require ( "./env" ) ;
2626const { HandlerCollection } = require ( "./shared/handlerCollection" ) ;
27- const { ENV , isNull } = require ( "./shared/static" ) ;
27+ const { ENV , isObject } = require ( "./shared/static" ) ;
2828const { promiseAllDone } = require ( "./shared/promiseAllDone" ) ;
2929const { LimitedLazyCache } = require ( "./shared/cache" ) ;
3030
@@ -278,27 +278,32 @@ class FeatureToggles {
278278 return [ { featureKey, errorMessage : "feature key is not valid" } ] ;
279279 }
280280
281- if ( scopeMap ) {
281+ if ( scopeMap !== undefined ) {
282+ if ( ! isObject ( scopeMap ) ) {
283+ return [
284+ {
285+ featureKey,
286+ errorMessage : "scopeMap must be undefined or an object" ,
287+ } ,
288+ ] ;
289+ }
282290 const allowedScopesCheckMap = this . __config [ featureKey ] [ CONFIG_KEY . ALLOWED_SCOPES_CHECK_MAP ] ;
283291 for ( const [ scope , value ] of Object . entries ( scopeMap ) ) {
284- if ( allowedScopesCheckMap && ! allowedScopesCheckMap [ scope ] ) {
292+ if ( ! FeatureToggles . _isValidScopeMapValue ( value ) ) {
285293 return [
286294 {
287295 featureKey,
288- scopeKey,
289- errorMessage : 'scope "{0}" is not allowed' ,
290- errorMessageValues : [ scope ] ,
296+ errorMessage : 'scope "{0}" has invalid type {1}, must be string' ,
297+ errorMessageValues : [ scope , typeof value ] ,
291298 } ,
292299 ] ;
293300 }
294- const scopeType = typeof value ;
295- if ( scopeType !== "string" ) {
301+ if ( allowedScopesCheckMap && ! allowedScopesCheckMap [ scope ] ) {
296302 return [
297303 {
298304 featureKey,
299- scopeKey,
300- errorMessage : 'scope "{0}" has invalid type {1}, must be string' ,
301- errorMessageValues : [ scope , scopeType ] ,
305+ errorMessage : 'scope "{0}" is not allowed' ,
306+ errorMessageValues : [ scope ] ,
302307 } ,
303308 ] ;
304309 }
@@ -453,7 +458,7 @@ class FeatureToggles {
453458 */
454459 async _validateFallbackValues ( fallbackValues ) {
455460 let validationErrors = [ ] ;
456- if ( isNull ( fallbackValues ) || typeof fallbackValues !== "object" ) {
461+ if ( ! isObject ( fallbackValues ) ) {
457462 return validationErrors ;
458463 }
459464
@@ -518,7 +523,7 @@ class FeatureToggles {
518523 this . __redisKey ,
519524 featureKey ,
520525 async ( scopedValues ) => {
521- if ( typeof scopedValues !== "object" || scopedValues === null ) {
526+ if ( ! isObject ( scopedValues ) ) {
522527 return null ;
523528 }
524529 const [ validatedScopedValues , scopedValidationErrors ] = await this . _validateScopedValues (
@@ -761,11 +766,34 @@ class FeatureToggles {
761766 // START OF GET_FEATURE_VALUE SECTION
762767 // ========================================
763768
769+ static _isValidScopeMapValue ( value ) {
770+ return typeof value === "string" ;
771+ }
772+
773+ /**
774+ * This is used to make sure scopeMap is either undefined or a shallow map with string entries. This happens for all
775+ * public interfaces with a scopeMap parameter, except {@link validateFeatureValue} and {@link changeFeatureValue}.
776+ * For these two interfaces, we want the "bad" scopeMaps to cause validation errors.
777+ * Also not for {@link getScopeKey}, where the sanitization must not happen in place.
778+ */
779+ static _sanitizeScopeMap ( scopeMap ) {
780+ if ( ! isObject ( scopeMap ) ) {
781+ return undefined ;
782+ }
783+ for ( const [ scope , value ] of Object . entries ( scopeMap ) ) {
784+ if ( ! FeatureToggles . _isValidScopeMapValue ( value ) ) {
785+ Reflect . deleteProperty ( scopeMap , scope ) ;
786+ }
787+ }
788+ return scopeMap ;
789+ }
790+
791+ // NOTE: getScopeMap does the scopeMap sanitization on the fly, because it must not modify scopeMap in place.
764792 static getScopeKey ( scopeMap ) {
765- if ( typeof scopeMap !== "object" || scopeMap === null ) {
793+ if ( ! isObject ( scopeMap ) ) {
766794 return SCOPE_ROOT_KEY ;
767795 }
768- const scopeMapKeys = Object . keys ( scopeMap ) ;
796+ const scopeMapKeys = Object . keys ( scopeMap ) . filter ( ( scope ) => FeatureToggles . _isValidScopeMapValue ( scopeMap [ scope ] ) ) ;
769797 if ( scopeMapKeys . length === 0 ) {
770798 return SCOPE_ROOT_KEY ;
771799 }
@@ -781,8 +809,8 @@ class FeatureToggles {
781809 // NOTE: there are multiple scopeMaps for every scopeKey with more than one inner entry. This will return the unique
782810 // scopeMap whose keys are sorted, i.e., matching the keys in the scopeKey.
783811 static getScopeMap ( scopeKey ) {
784- return typeof scopeKey !== "string" || scopeKey === SCOPE_ROOT_KEY
785- ? { }
812+ return ! this . _isValidScopeKey ( scopeKey ) || scopeKey === undefined || scopeKey === SCOPE_ROOT_KEY
813+ ? undefined
786814 : scopeKey . split ( SCOPE_KEY_OUTER_SEPARATOR ) . reduce ( ( acc , scopeInnerEntry ) => {
787815 const [ scopeInnerKey , value ] = scopeInnerEntry . split ( SCOPE_KEY_INNER_SEPARATOR ) ;
788816 acc [ scopeInnerKey ] = value ;
@@ -877,6 +905,7 @@ class FeatureToggles {
877905 */
878906 getFeatureValue ( featureKey , scopeMap = undefined ) {
879907 this . _ensureInitialized ( ) ;
908+ scopeMap = FeatureToggles . _sanitizeScopeMap ( scopeMap ) ;
880909 return FeatureToggles . _getFeatureValueForScopeAndStateAndFallback (
881910 this . __superScopeCache ,
882911 this . __stateScopedValues ,
0 commit comments