Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ModuleConfig.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions box.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
209 changes: 199 additions & 10 deletions interceptors/AutoClean.cfc
Original file line number Diff line number Diff line change
@@ -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;
}

}
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

********************************************************************************
Expand Down
10 changes: 10 additions & 0 deletions test-harness/config/Coldbox.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ component{
interceptors = [
];

moduleSettings = {
cbantisamy : {
autoCleanExclusions : {
"main.configDriven" : [ "apiToken" ],
"api-v1:Trips.*" : [ "moduleToken" ],
"api-v1:*" : [ "moduleSignature" ]
}
}
};

//LogBox DSL
logBox = {
// Define Appenders
Expand Down
14 changes: 9 additions & 5 deletions test-harness/handlers/Main.cfc
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
/**
* 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" ) );
}

// Run on first init
any function onAppInit( event, rc, prc ){
}

}
}
13 changes: 13 additions & 0 deletions test-harness/modules_app/api-v1/ModuleConfig.cfc
Original file line number Diff line number Diff line change
@@ -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 = {};
}

}
9 changes: 9 additions & 0 deletions test-harness/modules_app/api-v1/handlers/Trips.cfc
Original file line number Diff line number Diff line change
@@ -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" ) );
}

}
29 changes: 29 additions & 0 deletions test-harness/tests/specs/interceptor/AutoCleanTest.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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<script>alert('I am an attacker')</script>";
url.password = "secret<script>alert('leave me alone')</script>";
var event = execute( "main.index" );
var rc = event.getCollection();
expect( rc.data ).toBe( "guest" );
expect( rc.password ).toBe( "secret<script>alert('leave me alone')</script>" );
} );

it( "should skip auto clean exclusions configured for the current event", function(){
url.data = "guest<script>alert('I am an attacker')</script>";
url.apiToken = "token<script>alert('leave me alone')</script>";
var event = execute( "main.configDriven" );
var rc = event.getCollection();
expect( rc.data ).toBe( "guest" );
expect( rc.apiToken ).toBe( "token<script>alert('leave me alone')</script>" );
} );

it( "should match configured auto clean exclusions for module event patterns", function(){
url.data = "guest<script>alert('I am an attacker')</script>";
url.moduleToken = "token<script>alert('leave me alone')</script>";
url.moduleSignature = "signature<script>alert('leave me alone')</script>";
var event = execute( "api-v1:Trips.update" );
var rc = event.getCollection();
expect( rc.data ).toBe( "guest" );
expect( rc.moduleToken ).toBe( "token<script>alert('leave me alone')</script>" );
expect( rc.moduleSignature ).toBe( "signature<script>alert('leave me alone')</script>" );
} );
} );
}

Expand Down
Loading