Aaron Greenlee.com | A Personal Journey.
You should follow me here.
This is the RSS subscription you have been looking for.
 
Wrecking Ball Media
A great team that is clearing the way for digital marketing.

Have you thought about joining the Central Florida Web Developers User Group?

ColdFusion, ColdBox and ORM: Data Security

POSTED Wednesday, August 11, 2010  |   | DOWNLOAD CODE
Keywords: ColdBox, ColdFusion, ORM

I am excited about ColdFusion 9's ORM integration. Hibernate is a powerful tool and I think the ColdFusion developers at Adobe have implemented Hibernate exceptionally well. Since the release, I have been quite busy working on existing applications with my team at WRECKINGBALL Media and have not had enough time to invest in serious ORM development. Over the weekend, I was motivated to make the time.

Everything is in alignment for my ORM development. Adobe released ColdFusion 9.0.1 which improved the error messages (which frustrated me when since the release of CF9); ColdBox 3.0 M6 was just released which includes some very helpful ORM Services; and James Brown presented to the Central Florida Web Developers User Group on Getting Started with ColdFusion 9/Railo ORM.

As I surfed the Web to discover ColdFusion ORM examples, found messages in the CF-ORM Google Group and developed my own ORM application I realized how easily it would be to create an insecure application that allows a User to submit data you did not expect and have it persisted in the database. Fortunately, you can avoid this security issue by taking one little step during development.

ORM Security: Limit the data you accept.

To help illustrate this issue I have included some examples below: Example User Entity; a ColdBox Event Handler that is open to manipulation; and a secured ColdBox Event Handler.

Example User Entity
/** A User Example */
component
	extends='shared.model.object.BaseObject'
	cache=false autowire=false persistent=true {

	// Persisted Properties ---------------------------------------------------------
	property name='UserID' ormtype='integer' fieldtype='id' generator='native';

	// The following properties receive user input

	/** Used in user-user communications.
		Hyrule Data Validation Rules
	 	@Min 2
	  	@Max 25
	 	@String */
	property name='Username' ormtype='string' length='25';

	/** Used to determine if the User account is Active. If false, account is "deleted". */
	property name='Active' ormtype='boolean';

	/** Used to capture the password from the User.
	  	Hyrule Rules
		@Password 6,20,medium */
	property name='PasswordPlain' length='35' setter=false;

	/** Used only for validation on password changes.
		Hyrule Rules
		@IsMatch PasswordPlain */
	property name='PasswordConfirm' persistent=false length='35';
}

Consider we have a simple form that allows a user to change their password. This form only collects two fields from the user: "PasswordPlain" and "PasswordConfirm".

Form to Change Password
<p>Hello #rc.User.getUsername()#! Change your password here.</p>
<form action="#prc.xeh#" method="post">
	<!--- A common way to also send the User ID --->
	<input name="userid" type="hidden" value="#rc.User.getUserID()#" />
	<ol>
		<li>
			<label for="PasswordPlain">Password:</lable>
			<input name="PasswordPlain" type="password" />
		</li>
		<li>
			<label for="PasswordConfirm">Confirm Password:</lable>
			<input name="PasswordConfirm" type="password" />
		</li>
	</ol>
</form>

ColdBox--and the other frameworks-allow you to easily populate an object with user-input. ColdBox's BeanFactory Plugin can populate an object from a query, struct or the ColdBox Request Collection (RC). Unfortunately, in an ORM application-or any application that has a DAO that saves the entire state of an object-this often used technique allows the user to send anything in the form and if they guess the name right, your object will be populated and saved to the database.

Insecure Event Handler
/** Example ColdBox Event Handler */
component {
	/** Inject the ColdBox BeanFactory */
	property name='BeanFactory' inject='coldbox:plugin:BeanFactory';
	/** ColdBox will inject the new ColdBox ORMService */
	property name="ORMService" inject='coldbox:plugin:ORMService';
	/** Allow ColdBox to inject my UserService model */
	property name="UserService" inject='model';
	/** Allow ColdBox to inject my ValidationModel which wraps
		Hyrule and sets any errors into my Object */
	property name="ValidationService" inject='model';

	// PUBLIC ACTIONS ---------------------------------------------------------------

	/** Accept and validate the user changing their password. */
	public void function validateChange (required Event) {
		var rc 	= Event.getCollection();
		var prc = Event.getCollection(true);

		// Allow ColdBox's ORMService to provide us
		// with an existing USER Entity
		rc.User = ORMService.get('User', rc.UserID);

		// Verify the return of the User
		if (isNull(rc.User))
			setNextEvent('User.BadUser');

		// Populate User with the user's input
		BeanFactory.populateBean(rc.User);

		// Data Validation
		if (!ValidationService.validate(rc.User)) {
			// Invalid User Input. Send back to the form.
			Flash.persistRC('User');
			setNextEvent('user.changePassword');
		}

		// The data is valid, so, save the User's new password
		ORMService.save(rc.User);

		setNextEvent('user.showUpdate');
	}
}

In the above example, the BeanFactory plugin will populate any property in your object with values provided by the user so long as a "setter" method exists. If the user wanted to also change their username they could easily do so by simply appending "?username=myNewName" to the URL of the action page or use FireBug or other tools to inject unexpected values. This could be a real issue if you use properties like "active" or "deleted". With a few attempts, a malicious user may be able to delete or deactivate another user's account!

This simple security issue can be resolved with almost no work. Just provide the "include" argument to the BeanFactory's populateBean method to explicitly limit the values we wish to accept from the user. I have highlighted the added line in the following example:

Secure Event Handler
/** Example ColdBox Event Handler */
component {
	/** Inject the ColdBox BeanFactory */
	property name='BeanFactory' inject='coldbox:plugin:BeanFactory';
	/** ColdBox will inject the new ColdBox ORMService */
	property name="ORMService" inject='coldbox:plugin:ORMService';
	/** Allow ColdBox to inject my UserService model */
	property name="UserService" inject='model';
	/** Allow ColdBox to inject my ValidationModel which wraps
		Hyrule and sets any errors into my Object */
	property name="ValidationService" inject='model';

	// PUBLIC ACTIONS ---------------------------------------------------------------

	/** Accept and validate the user changing their password. */
	public void function validateChange (required Event) {
		var rc 	= Event.getCollection();
		var prc = Event.getCollection(true);

		// Allow ColdBox's ORMService to provide us
		// with an existing USER Entity
		rc.User = ORMService.get('User', rc.UserID);

		// Verify the return of the User
		if (isNull(rc.User))
			setNextEvent('User.BadUser');

		// Populate User with the user's input
		BeanFactory.populateBean(
			 target=rc.User
			// THIS IS THE IMPORTANT LINE
			,include='passwordPlain,passwordConfirm');

		// Data Validation
		if (!ValidationService.validate(rc.User)) {
			// Invalid User Input. Send back to the form.
			Flash.persistRC('User');
			setNextEvent('user.changePassword');
		}

		// The data is valid, so, save the User's new password
		ORMService.save(rc.User);

		setNextEvent('user.showUpdate');
	}
}

There are many ways to secure your application with ColdBox. This solution addresses a potential issue where a valid user could modify values they should not have access to. For example, a user should not have access to the "active" property in the example object. Credit to John Whish for tweeting other solutions including the use of the Security Interceptor and ColdBox's Event Handler "this.allowedMethods" settings to mitigate much of the risk of users manipulating data they should not have access to. The above solution is recomended to help limit the ability of valid, accepted users from providing data they should not be able to directly manipulate.

Related Code

The following code is realated to this posting and is provided as reference. The BaseObject is extended by the User entity. The ValidationService is simply a wrapper around Hyrule.

Base Object
/*-------------------------------------------------------------------------
Author			: 	Aaron Greenlee (http://www.aarongreenlee.com)
Date Created	: 	8/10/2010 5:29:47 PM
Description		: 	The Base Object can be extended by others and has common methods.
Modified		: 	N/A
 --------------------------------------------------------------------------*/

/** @hint The Base Object can be extended by others and has common methods. */
component name='BaseObject' cache=false accessors=true {

	/** Allow errors to be set within the object */
	property name='Errors' setter=false;

	/** Constructor. */
	public shared.model.object.BaseObject function init () {
		errors = [];
		errorProperties = [];
		return this;
	}

	// Public Methods--------------------------------------------------------------------

	/** Erase all errors from the Object */
	public void function clearErrors() {
		variables.errors = [];
		variables.errorProperties= [];
		return;
	}

	/** Set multiple errors into the Object
		@Errors shared.model.object.Error[] */
	public void function setErrors(required array Errors) {
		var i = 0;
		var n = arrayLen(arguments.Errors);
		for (i=1; i<=n; i++)
			addError(arguments.errors[i]);

		return;
	}

	/** Add an error object. Also saves the property to a errorProperties array for quick searching.
		@Error shared.model.object.Error */
	public void function addError(required Error) {
		if (structKeyExists(variables, 'errors')) {
			arrayAppend(errors, arguments.error);
			arrayAppend(errorProperties, arguments.error.getProperty() );
		} else {
			variables.errors = [ arguments.error ];
			variables.errorProperties = [ arguments.error.getProperty() ];
		}

		return;
	}

	/** Returns true if any error exist. */
	public boolean function hasErrors () {
		if (structKeyExists(variables, 'errors'))
			return !arrayIsEmpty(errors);

		return false;
	}

	/** Returns true if the property is in error
		@property The name of the property to check for errors. */
	public boolean function errorExists(required string property) {

		if (!hasErrors())
			return false;

		return arrayContains(variables.errorProperties, property);
	}
}
Validation Service
/*-------------------------------------------------------------------------
Author			: 	Aaron Greenlee (http://www.aarongreenlee.com)
Date Created	: 	8/10/2010 5:22:51 PM
Description		: 	Object Validation Service.
Modified		: 	N/A
 --------------------------------------------------------------------------*/

/** @hint Object Validation Service. */
component
	name="ValidationService"
	singleton  {

	// CONSTRUCTOR	---------------------------------------------
	public shared.models.services.ValidationService function init() {
		variables.Hyrule = new hyrule.Validator();
		return this;
	}

	// PUBLIC -----------------------------------------------------------------------

	/** @hint Return the validion library. */
	public hyrule.Validator function getValidator() {
		return variables.Hyrule;
	}

	/** Validate an Object. Errors will be set into the Object
		@Object The object to be validated.
		@Include A list. If provided, only properties listed that are in error will be parsed.
		@Exclude A list. If provided, all properties except these will be parsed for errors. Ignored if "include" is provided. */
	public boolean function validate(required any Object, string include, string exclude) {
		Object.clearErrors();

		var errors = variables.Hyrule.validate(Object);

		// Return TRUE if we have no Errors from Hyrule.
		if (!errors.hasErrors())
			return true;

		// Filter errors if desired
		if (structKeyExists(arguments, 'include'))
			var results = filterErrors(errors=errors,include=arguments.include);
		else if (structKeyExists(arguments, 'exclude'))
			var results = filterErrors(errors=errors,excelude=arguments.exclude);

		// Set the errors in the object and return false
		Object.setErrors( errors.getErrors() );
		return false;
	}

	/** Removes any errors not found within the list. Useful for partial object validation.
		@include A list of properties to retain if they exist within the error list. */
	private array function filterErrors(required array errors, string include, string exclude) {

		// THIS METHOD NOT TESTED BEFORE PUBLICATION.
		// Posted in response to discussion in the ColdBox forum.

		var i 				= 0;
		var n 				= arrayLen(errors);
		var retainedErrors 	= [];
		var exclusion 		= structKeyExists(arguments, 'exclude');
		var props			= (exclusion) ? arrayToList(arguments.exclude) : arrayToList(arguments.include);

		if (!exclusion) {
			// Include any errors found within the list.
			for (i=1; i<=n; i++)
				if ( arrayContains(props, errors[i].getProperty()) )
					arrayAppend(retainedErrors, errors[i]);
		} else {
			// Copy all errors
			retainedErrors = errors;
			// Exclude any found errors from the list.
			for (i=1; i<=n; i++)
				if ( arrayContains(props, errors[i].getProperty()) )
					arrayDeleteAt(retainedErrors, i);
		}

		return retainedErrors;
	}
}
blog comments powered by Disqus
You can send me email or work with
   me for digital marketing, web design
      and application development.

         I own proprietary web application development
         company and work with a leading digital marketing firm.
© 2009 - 2012 Aaron Greenlee. Powered by my own code on the ColdBox Framework.

This site is best viewed on Chrome, FireFox and Safari. Subscribe to my RSS feed.