diff --git a/ModuleConfig.cfc b/ModuleConfig.cfc
index 22e1fe3..dc2624d 100644
--- a/ModuleConfig.cfc
+++ b/ModuleConfig.cfc
@@ -28,6 +28,8 @@ component {
autoRegisterInterceptor : true,
// Activate auto request capture cleanups
autoClean : true,
+ // Keys to exclude from auto clean. Use an array/list for global keys, or a struct with "*", "handler.action", or "handler.*" keys
+ autoCleanExclusions : {},
// Default Policy to use, available are: antisamy, ebay, myspace, slashdot and tinymce
defaultPolicy : "ebay",
// Custom Policy absolute path, leave empty if not used
diff --git a/box.json b/box.json
index f6af2a7..99954a5 100644
--- a/box.json
+++ b/box.json
@@ -42,9 +42,9 @@
"build:docs":"task run taskFile=build/Build.cfc target=docs :projectName=`package show slug` :version=`package show version`",
"install:dependencies":"install && cd test-harness && install",
"release":"recipe build/release.boxr",
- "format":"cfformat run helpers,models,test-harness/tests/,ModuleConfig.cfc --overwrite",
- "format:watch":"cfformat watch helpers,models,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json",
- "format:check":"cfformat check helpers,models,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json",
+ "format":"cfformat run helpers,interceptors,models,test-harness/tests/,ModuleConfig.cfc --overwrite",
+ "format:watch":"cfformat watch helpers,interceptors,models,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json",
+ "format:check":"cfformat check helpers,interceptors,models,test-harness/tests/,ModuleConfig.cfc ./.cfformat.json",
"start:lucee":"server start serverConfigFile=server-lucee@5.json",
"start:2021":"server start serverConfigFile=server-adobe@2021.json",
"stop:lucee":"server stop serverConfigFile=server-lucee@5.json",
diff --git a/interceptors/AutoClean.cfc b/interceptors/AutoClean.cfc
index 7bcf9e6..5b42c12 100644
--- a/interceptors/AutoClean.cfc
+++ b/interceptors/AutoClean.cfc
@@ -1,29 +1,218 @@
/**
-* Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp
-* www.ortussolutions.com
-* ---
-* This Interceptor if activated automatically cleans the request collection for you
-*/
-component extends="coldbox.system.Interceptor"{
+ * Copyright 2005-2007 ColdBox Framework by Luis Majano and Ortus Solutions, Corp
+ * www.ortussolutions.com
+ * ---
+ * This Interceptor if activated automatically cleans the request collection for you
+ */
+component extends="coldbox.system.Interceptor" {
// DI: This is a provider as it needs to javaloaded first
property name="antisamy" inject="provider:AntiSamy@CBAntiSamy";
+ // The handler method annotation used to exclude request collection keys from auto-cleaning
+ variables.AUTO_CLEAN_EXCLUSIONS_ANNOTATION = "antisamyAutoCleanExclusions";
+
// On request capture
function onRequestCapture( event, interceptData, buffer, rc, prc ){
// if not activated, just exit
- if( !getModuleSettings( "cbantisamy", "autoClean" ) || !event.getPrivateValue( "antisamy-autoclean", true ) ){
+ if ( !getModuleSettings( "cbantisamy", "autoClean" ) || !event.getPrivateValue( "antisamy-autoclean", true ) ) {
return;
}
- rc.keyArray().each(
- function( key ){
- if( !isNull( rc[ key ] ) && isSimpleValue( rc[ key ] ) ){
+ var exclusions = getAutoCleanExclusions( event );
+
+ rc.keyArray()
+ .each( function( key ){
+ if ( !arrayFindNoCase( exclusions, key ) && !isNull( rc[ key ] ) && isSimpleValue( rc[ key ] ) ) {
rc[ key ] = variables.antiSamy.clean( rc[ key ] );
}
+ } );
+ }
+
+ /**
+ * Resolve the request collection keys excluded from auto-cleaning for this event action.
+ */
+ private array function getAutoCleanExclusions( required event ){
+ var exclusions = [];
+
+ exclusions.append( getConfiguredAutoCleanExclusions( arguments.event ), true );
+
+ exclusions.append(
+ normalizeAutoCleanExclusionKeys( getActionAutoCleanExclusionsAnnotation( arguments.event ) ),
+ true
+ );
+
+ exclusions.append(
+ normalizeAutoCleanExclusionKeys(
+ arguments.event.getPrivateValue( "antisamy-autoclean-exclusions", [] )
+ ),
+ true
+ );
+
+ return exclusions;
+ }
+
+ /**
+ * Resolve configured auto-clean exclusions for the current event.
+ */
+ private array function getConfiguredAutoCleanExclusions( required event ){
+ var settings = getModuleSettings( "cbantisamy" );
+
+ if ( !settings.keyExists( "autoCleanExclusions" ) ) {
+ return [];
+ }
+
+ if ( !isStruct( settings.autoCleanExclusions ) ) {
+ return normalizeAutoCleanExclusionKeys( settings.autoCleanExclusions );
+ }
+
+ var exclusions = [];
+ var patterns = getAutoCleanExclusionEventPatterns( arguments.event );
+
+ settings.autoCleanExclusions.each( function( pattern, keys ){
+ if ( arrayFindNoCase( patterns, pattern ) ) {
+ exclusions.append( normalizeAutoCleanExclusionKeys( keys ), true );
+ }
+ } );
+
+ return exclusions;
+ }
+
+ /**
+ * Build the event patterns checked for configured auto-clean exclusions.
+ */
+ private array function getAutoCleanExclusionEventPatterns( required event ){
+ var currentEvent = arguments.event.getCurrentEvent();
+ var patterns = [ "*" ];
+
+ if ( len( currentEvent ) ) {
+ patterns.append( currentEvent );
+ patterns.append( getEventHandlerPattern( currentEvent ) );
+
+ if ( find( ":", currentEvent ) ) {
+ patterns.append( listFirst( currentEvent, ":" ) & ":*" );
+ }
+ }
+
+ return patterns;
+ }
+
+ /**
+ * Build an event handler wildcard pattern, preserving ColdBox module event prefixes.
+ */
+ private string function getEventHandlerPattern( required string event ){
+ return listFirst( arguments.event, "." ) & ".*";
+ }
+
+ /**
+ * Read the target handler action's auto-clean exclusions annotation.
+ */
+ private any function getActionAutoCleanExclusionsAnnotation( required event ){
+ var currentEvent = arguments.event.getCurrentEvent();
+
+ if ( !len( currentEvent ) ) {
+ return [];
+ }
+
+ try {
+ var handlerService = controller.getHandlerService();
+ var handlerBean = handlerService.getHandlerBean( currentEvent );
+ var action = handlerBean.getMethod();
+ var actionMetadata = getComponentActionMetadata( handlerBean.getRunnable(), action );
+
+ if ( structIsEmpty( actionMetadata ) ) {
+ var handler = handlerService.newHandler( handlerBean );
+ actionMetadata = structKeyExists( handler, action ) ? getMetadata( handler[ action ] ) : handler._actionMetadata(
+ action
+ );
+ }
+
+ return getAutoCleanExclusionsAnnotationValue( actionMetadata );
+ } catch ( any e ) {
+ return [];
+ }
+ }
+
+ /**
+ * Find action metadata by reading the handler CFC metadata directly.
+ */
+ private struct function getComponentActionMetadata( required string componentPath, required string action ){
+ var componentMetadata = getComponentMetadata( arguments.componentPath );
+
+ if ( !componentMetadata.keyExists( "functions" ) ) {
+ return {};
+ }
+
+ for ( var functionMetadata in componentMetadata.functions ) {
+ if (
+ functionMetadata.keyExists( "name" ) && compareNoCase( functionMetadata.name, arguments.action ) == 0
+ ) {
+ return functionMetadata;
}
+ }
+
+ return {};
+ }
+
+ /**
+ * Read the auto-clean exclusions value from function metadata or docblock annotations.
+ */
+ private any function getAutoCleanExclusionsAnnotationValue( required struct actionMetadata ){
+ var exclusions = getStructValueNoCase(
+ arguments.actionMetadata,
+ variables.AUTO_CLEAN_EXCLUSIONS_ANNOTATION
);
+ if ( !isNull( exclusions ) ) {
+ return exclusions;
+ }
+
+ if ( arguments.actionMetadata.keyExists( "annotations" ) ) {
+ exclusions = getStructValueNoCase(
+ arguments.actionMetadata.annotations,
+ variables.AUTO_CLEAN_EXCLUSIONS_ANNOTATION
+ );
+
+ if ( !isNull( exclusions ) ) {
+ return exclusions;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Find a struct value by key without relying on the engine's key case behavior.
+ */
+ private any function getStructValueNoCase( required struct target, required string key ){
+ for ( var targetKey in arguments.target ) {
+ if ( compareNoCase( targetKey, arguments.key ) == 0 ) {
+ return arguments.target[ targetKey ];
+ }
+ }
+ }
+
+ /**
+ * Normalize an exclusion value to an array of key names.
+ */
+ private array function normalizeAutoCleanExclusionKeys( required any keys ){
+ var normalizedKeys = [];
+
+ if ( isArray( arguments.keys ) ) {
+ arguments.keys.each( function( key ){
+ if ( isSimpleValue( key ) && len( trim( key ) ) ) {
+ normalizedKeys.append( trim( key ) );
+ }
+ } );
+ } else if ( isSimpleValue( arguments.keys ) ) {
+ listToArray( arguments.keys ).each( function( key ){
+ if ( len( trim( key ) ) ) {
+ normalizedKeys.append( trim( key ) );
+ }
+ } );
+ }
+
+ return normalizedKeys;
}
}
diff --git a/readme.md b/readme.md
index 05b6949..5bcc602 100644
--- a/readme.md
+++ b/readme.md
@@ -89,6 +89,14 @@ moduleSettings = {
autoRegisterInterceptor = true,
// Activate auto request capture cleanups interceptor
autoClean = true,
+ // Exclude request collection keys from auto clean globally or by event pattern
+ autoCleanExclusions = {
+ "*" = [ "csrfToken" ],
+ "main.login" = [ "password" ],
+ "api.*" = [ "payloadJSON" ],
+ "api-v1:Trips.*" = [ "rawNotes" ],
+ "api-v1:*" = [ "requestSignature" ]
+ },
// Default Policy to use, available are: antisamy, ebay, myspace, slashdot and tinymce
defaultPolicy = "ebay",
// Custom Policy absolute path, leave empty if not used
@@ -97,6 +105,18 @@ moduleSettings = {
};
```
+### Auto Clean Action Exclusions
+
+The auto clean interceptor cleans every simple value in the request collection by default. If an action needs to receive a raw value, add the `antisamyAutoCleanExclusions` annotation to the handler method with a comma-delimited list of request collection keys to skip:
+
+```js
+function login( event, rc, prc ) antisamyAutoCleanExclusions="password"{
+ // rc.password is not cleaned by the auto clean interceptor for this action.
+}
+```
+
+You can also configure exclusions in module settings using `*` for global exclusions, exact events like `main.login`, handler wildcards like `api.*`, module handler wildcards like `api-v1:Trips.*`, or module wildcards like `api-v1:*`. Configured exclusions and action annotations are merged for the current request.
+
You can read more about AntiSamy here: https://www.owasp.org/index.php/Category:OWASP_AntiSamy_Project
********************************************************************************
diff --git a/test-harness/config/Coldbox.cfc b/test-harness/config/Coldbox.cfc
index 290e6d3..9d93d08 100644
--- a/test-harness/config/Coldbox.cfc
+++ b/test-harness/config/Coldbox.cfc
@@ -52,6 +52,16 @@ component{
interceptors = [
];
+ moduleSettings = {
+ cbantisamy : {
+ autoCleanExclusions : {
+ "main.configDriven" : [ "apiToken" ],
+ "api-v1:Trips.*" : [ "moduleToken" ],
+ "api-v1:*" : [ "moduleSignature" ]
+ }
+ }
+ };
+
//LogBox DSL
logBox = {
// Define Appenders
diff --git a/test-harness/handlers/Main.cfc b/test-harness/handlers/Main.cfc
index 2898fb9..ba3848d 100644
--- a/test-harness/handlers/Main.cfc
+++ b/test-harness/handlers/Main.cfc
@@ -1,12 +1,16 @@
/**
-* My Event Handler Hint
-*/
-component{
+ * My Event Handler Hint
+ */
+component {
property name="antisamy" inject="antisamy@cbantisamy";
// Index
- any function index( event,rc, prc ){
+ any function index( event, rc, prc ) antisamyAutoCleanExclusions="password"{
+ rc.data = antisamy.clean( event.getValue( "data", "no data sent" ) );
+ }
+
+ any function configDriven( event, rc, prc ){
rc.data = antisamy.clean( event.getValue( "data", "no data sent" ) );
}
@@ -14,4 +18,4 @@ component{
any function onAppInit( event, rc, prc ){
}
-}
\ No newline at end of file
+}
diff --git a/test-harness/modules_app/api-v1/ModuleConfig.cfc b/test-harness/modules_app/api-v1/ModuleConfig.cfc
new file mode 100644
index 0000000..4bbe517
--- /dev/null
+++ b/test-harness/modules_app/api-v1/ModuleConfig.cfc
@@ -0,0 +1,13 @@
+component {
+
+ this.title = "API v1";
+ this.author = "Ortus Solutions, Corp";
+ this.description = "Test module for cbantisamy module event coverage";
+ this.version = "1.0.0";
+ this.cfmapping = "apiV1";
+
+ function configure(){
+ settings = {};
+ }
+
+}
diff --git a/test-harness/modules_app/api-v1/handlers/Trips.cfc b/test-harness/modules_app/api-v1/handlers/Trips.cfc
new file mode 100644
index 0000000..d90b3ce
--- /dev/null
+++ b/test-harness/modules_app/api-v1/handlers/Trips.cfc
@@ -0,0 +1,9 @@
+component {
+
+ property name="antisamy" inject="antisamy@cbantisamy";
+
+ any function update( event, rc, prc ){
+ rc.data = antisamy.clean( event.getValue( "data", "no data sent" ) );
+ }
+
+}
diff --git a/test-harness/tests/specs/interceptor/AutoCleanTest.cfc b/test-harness/tests/specs/interceptor/AutoCleanTest.cfc
index 0c20f72..2f61ede 100644
--- a/test-harness/tests/specs/interceptor/AutoCleanTest.cfc
+++ b/test-harness/tests/specs/interceptor/AutoCleanTest.cfc
@@ -30,6 +30,35 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
var rc = event.getCollection();
expect( rc.data ).toBe( "guest" );
} );
+
+ it( "should skip auto clean exclusions annotated on the current action", function(){
+ url.data = "guest";
+ url.password = "secret";
+ var event = execute( "main.index" );
+ var rc = event.getCollection();
+ expect( rc.data ).toBe( "guest" );
+ expect( rc.password ).toBe( "secret" );
+ } );
+
+ it( "should skip auto clean exclusions configured for the current event", function(){
+ url.data = "guest";
+ url.apiToken = "token";
+ var event = execute( "main.configDriven" );
+ var rc = event.getCollection();
+ expect( rc.data ).toBe( "guest" );
+ expect( rc.apiToken ).toBe( "token" );
+ } );
+
+ it( "should match configured auto clean exclusions for module event patterns", function(){
+ url.data = "guest";
+ url.moduleToken = "token";
+ url.moduleSignature = "signature";
+ var event = execute( "api-v1:Trips.update" );
+ var rc = event.getCollection();
+ expect( rc.data ).toBe( "guest" );
+ expect( rc.moduleToken ).toBe( "token" );
+ expect( rc.moduleSignature ).toBe( "signature" );
+ } );
} );
}