Skip to main content
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Jamie Ly
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Jamie Ly ( @jamiealy )

Snooper.cfc - A ColdFusion Component For Finding Live Variable Leaks

By on
Tags:

Hopefully, variable leaks never make their way into a production application. But, sometimes they do. And, the more cooks you have in the kitchen, the more likely this is to happen. Variable leaks or, improperly scoped variables, can be insanely frustrating to track down because they don't "explode"; rather, they lead to unexpected and generally inconsistent and hard-to-reproduce behavior. To help find these production problems, I use a small ColdFusion component - Snooper.cfc - that can iterate over the private variables of a cached component. And, while Snooper.cfc isn't smart enough to find variable leaks on its own, it can provide me with the information that I need to track them down and fix them.

Project: See the Snooper.cfc on my GitHub account.

First, a word of caution:

NEVER RUN THIS IN PUBLIC!

Private variables are meant to be private and can include sensitive information like passwords and API keys. Never allow this kind of debugging to be done out in the open. This should always be hidden behind a security wall, or even better, behind a VPN (Virtual Private Network).

That said, snooping private variables consists of creating a page that pulls cached ColdFusion components out of memory, snoops them, and then dumps the results to the screen where I can visually scan the output looking for problematic values. To see this in action, here's a small sample snoop page:

<cfscript>

	// Grab the cached value and perform a function that we know has a variable leak.
	application.thing.leakForLoop();

	// Get the manifest of private variables that may be leaking.
	manifest = new lib.Snooper()
		// .includeFunctions()
		// .includeComponents()
		.excludeKeys( [ "password", "secretkey", "apiKey" ] )
		.snoop( application.thing )
	;

</cfscript>

<!--- ------------------------------------------------------------------------------ --->
<!--- ------------------------------------------------------------------------------ --->

<cfcontent type="text/html; charset=utf-8" />

<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>
		Checking ColdFusion Variable Leaks With Snooper.cfc
	</title>
</head>
<body>

	<h1>
		Checking ColdFusion Variable Leaks With Snooper.cfc
	</h1>

	<cfdump
		label="(Potentially) Leaked Variables"
		var="#manifest#"
		format="html"
	/>

</body>
</html>

As you can see, I'm pulling the cached Thing.cfc instance out of memory and calling .snoop() on it. When we run this code, we get the following output:

Snooper.cfc - a ColdFusion component to help track down live variable leaks in a production system.

Here, you can see that the variable, "i", is showing up on the private scope. While that doesn't mean that this is "wrong", variables with names like that are generally used as iteration variables that should be locally scoped to a function. As such, this would make a great candidate for further investigation.

By default, private functions and private objects are excluded from the key manifest. This is because variable leaks are generally going to be caused by simple values (strings and numbers) and lower-level complex objects like queries, structs, and arrays. Generally, functions are not leaks and references to ColdFusion components are not leaks. But, you can override the defaults by calling:

  • .includeComponents()
  • .includeFunctions()

... before calling .snoop().

To reduce noise (and reduce possible security problems), you can also exclude arbitrary keys by passing in an array of keys to blacklist during the snoop operation:

  • .excludeKeys( arrayOfKeys )

Each key is individually matched. However, if you need more granular control over whether or not to report a key, you can pass an optional filter function to the .snoop() method:

  • .snoop( target, function( key, value ) { return( true ); } )

The filter function returns True for keys that should be included and False (or Falsey) for keys that should be excluded from the manifest.

Here's a quick look at the Snooper.cfc ColdFusion component itself:

component
	output = false
	hint = "I provide a means to examine ColdFusion components for variable leaks."
	{

	/**
	* I create a new Snooper that can inspect private variables.
	*
	* @output false
	*/
	public any function init() {

		// By default, we're going to exclude Functions and Components in the private
		// variable reporting. More likely than not, these are not leaked variables.
		// Leaks tend to be caused by lower-level values like numbers, strings, arrays
		// and structs.
		isIncludingFunctions = false;
		isIncludingComponents = false;

		// By default, we won't exclude any arbitrary keys from the manifest.
		keysToExclude = [];

		return( this );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	/**
	* I exclude ColdFusion components from the collection of reported private keys.
	* Returns [this].
	*
	* @output false
	*/
	public any function excludeComponents() {

		isIncludingComponents = false;

		return( this );

	}


	/**
	* I exclude ColdFusion functions from the collection of reported private keys.
	* This does NOT EXCLUDE closures as those are more likely be a source of leaks.
	* Returns [this].
	*
	* @output false
	*/
	public any function excludeFunctions() {

		isIncludingFunctions = false;

		return( this );

	}


	/**
	* I exclude the given keys from every manifest. Returns [this].
	*
	* @newKeys I am an array of key values to always exclude.
	* @output false
	*/
	public any function excludeKeys( required array newKeys ) {

		// Reset the key exclusion collection.
		keysToExclude = [];

		for ( var newKey in newKeys ) {

			if ( isSimpleValue( newKey ) ) {

				arrayAppend( keysToExclude, lcase( newKey ) );

			}

		}

		return( this );

	}


	/**
	* I include ColdFusion components in the collection of reported private keys.
	* Returns [this].
	*
	* @output false
	*/
	public any function includeComponents() {

		isIncludingComponents = true;

		return( this );

	}


	/**
	* I include ColdFusion functions in the collection of reported private keys.
	* Returns [this].
	*
	* @output false
	*/
	public any function includeFunctions() {

		isIncludingFunctions = true;

		return( this );

	}


	/**
	* I inspect the private variables of the given target. By default, all keys will
	* be examined; however, you can include an optional filter that can exclude reported
	* keys (to both reduce noise and reduce security issues).
	*
	* I return a struct manifest of the private keys being reported.
	*
	* @target I am the ColdFusion component being snooped.
	* @filter I am the optional filter for key inclusion (return True to include).
	* @output false
	*/
	public struct function snoop(
		required any target,
		function filter = noopTrue
		) {

		testTarget( target );

		// Echo the name of the target component for a more flexible consumption in the
		// calling context.
		var manifest = {
			target: getMetaData( target ).name,
			keys: {},
			keyCount: 0
		};

		// In order to gain access to the private variables of the target, we have to
		// inject a Function that will circumvent the private security. However, after
		// we access the private variables we have to clean up after ourselves - we
		// don't want to leave the target in a dirty state.
		try {

			target.snoop___getVariables = snoop___getVariables;

			var privateVariables = target.snoop___getVariables();

		} finally {

			structDelete( target, "snoop___getVariables" );

		}

		// Now that we have our extracted private variables, let's iterate over the keys
		// and determine which ones should be reported.
		for ( var privateKey in privateVariables ) {

			// Normalize the key for consistent consumption.
			privateKey = lcase( privateKey );

			var privateValue = privateVariables[ privateKey ];

			// Exclude private components if necessary.
			if ( isObject( privateValue ) && ! isIncludingComponents ) {

				continue;

			}

			// Excluded private functions if necessary.
			if ( isCustomFunction( privateValue ) && ! isIncludingFunctions ) {

				continue;

			}

			// Exclude blacklisted keys if necessary.
			if ( arrayContains( keysToExclude, privateKey ) ) {

				continue;

			}

			// Exclude private key based on user-provided filter.
			if ( ! filter( privateKey, privateValue ) ) {

				continue;

			}

			// If we made it this far, the user wants to see this private variable
			// reported in the snooped manifest.
			manifest.keys[ privateKey ] = privateValue;
			manifest.keyCount++;

		}

		return( manifest );

	}


	/**
	* I am the INJECTED method that exposes the private Variables scope of the target.
	* After being injected, it is called in the context of the target, hence the just-in
	* time variables binding.
	*
	* @output false
	*/
	public struct function snoop___getVariables() {

		return( variables );

	}


	/**
	* I test the given target to see if it can be snooped. If the target is valid, I
	* return quietly; otherwise, I throw an error.
	*
	* @newTarget I am the target being validated.
	* @output false
	*/
	public void function testTarget( required any newTarget ) {

		if ( ! isObject( newTarget ) ) {

			throw(
				type = "Snooper.InvalidTarget",
				message = "Target must be a ColdFusion component."
			);

		}

	}


	// ---
	// PRIVATE METHODS.
	// ---


	/**
	* I always return true.
	*
	* @output false
	*/
	private boolean function noopTrue() {

		return( true );

	}

}

Anyway, I've found this approach to be extremely useful over the past few years. It's helped me track down some of the most infuriating problems. Maybe it can help someone else.

Want to use code from this post? Check out the license.

Reader Comments

18 Comments

What are some of the "unexpected and generally inconsistent and hard-to-reproduce" behaviors you've experienced from leaked variables? I think explicit examples will help to provide context and reference for users that have this problem, but may not realize it.

15,674 Comments

@JC,

Sure thing, great question. Some of the race conditions can be HORRIBLE. I'm talking about situations where you accidentally expose one user's data to another user. Imagine you have a method for some Contacts app that is like:

function getContacts( userID ) {
. . . . var user = getUserByID( userID );
. . . . contacts = getContactsByUserID( user.id );
.
. . . . logAccess( userID );
.
. . . . return( contacts );
}

In this case, I am *forgetting* to *var* the "contacts" value. This means that it is getting stored in the variables scope of the my service object. Which means it is not being shared among every method call to this object. So, imagine that two different users make a request that routes through this method at the same time:

User A asks for contacts.
User A's contacts get stored in "contacts".
========
User B asks for contacts.
User B's contacts *overwrite* the "contacts" variable.
========
User A returns the *shared* "contacts" variable, which has been overwritten by User B.

... now, if User A's request hasn't finished yet (maybe because the logAccess() method is slower for A than for B), by the time User A gets to "return( contacts )", they will actually end up returning the value that was overwritten by User B.

In the end, User A ends of retrieving User B's contacts.... which is a very bad thing.

But, this only happens if the race-condition timing actually happens. Which it may not always. Which is why things like this are super hard to debug.

Another common one is with index-based for-loops. Imagine that I have some code that is like:

for ( i = 1 ; i <= N ; i++ ) {
. . . . arrayAppend( data, getThings( i ) );
}

Here, I am *forgetting* to *var* "i", the iteration variable. Now, if I have two users that are hitting this code at the same time, one user's for-loop incrementing will end up affecting the "i" value of the other user (since they are both using the same "i" variable which is accidentally shared in the Variables scope).

This could lead to duplicate values in the response. Or, missing sections of the loop in other responses.

Because these are all break based on race-conditions, they are particularly hard to spot. Of course, you can always debug them locally with Snooper as well. Just run you code, go through all the workflows, and then see what is sticking around in the Variable scope. The only reason this is generally harder to do locally is because:

1) You are probably forgetting all the possible workflows (or there are too many to test).
2) You are constantly setting up and *tearing down* the application which means that nothing is persisted locally.

So, you do your best locally to spot these problems. But, you use this in production as a sanity check.

4 Comments

Ben,

For components written in CFML i use ( http://varscoper.riaforge.org/ ) but it doesn't work with components written in cfscript.

Snooper.cfc looks interesting for cfscript components albeit the method for checking your code is different as you can't just run it directly against your source.

I do have a question. If you add the following lines under leakforloop() in thing.cfc:
variables.tom = 'Tom';
arguments.tommy = 'Tommy';
local.thomas = 'Thomas';
and then run index.cfm you see key "tom" included in your list of potentially leaked variables. Am I missing a way to exclude explicitly scoped variables scoped variables from the result or is it not possible at present?

Note, i performed this test on CF10.

Cheers
Tom

4 Comments

Ben,

Follow on from above just for clarity i mean a way to block all variables in the variables scope not just variable "tom" which could be blocked by just adding "tom" to .excludeKeys.

Cheers
Tom

15,674 Comments

@Tom,

You won't have to worry about keys that are locally (ie, var-scoped) or scoped to the arguments collection as those won't be found when Snooper goes through the Variables scope. But, not alllll variable-scoped values are bad. After all, it's the "private scope" and there's lot of private data that you want to keep private. That's why you can blacklist and filter values from being reported.

But, more than anything, that's why this is a bit more art than it is science. Once you have the output, you have to scan over it manually to see if there is anything there that *looks funny*. That said, I'm totally open to suggestions on how to improve the process. Right now, I do it manually.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel