Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Scope Traversal Behavior With Undefined Function Arguments In Lucee CFML 5.3.6.61

By Ben Nadel on
Tags: ColdFusion

Just now, as I was looking at iterating over Structs using CFLoop in Lucee CFML, I ran into a fun little behavior of the ColdFusion language: Scope traversal will skip-over undefined function arguments and access like-named values in higher-up scopes. At first, this feels like a bug. But, I think it ends-up being consistent with how ColdFusion has always handled scope traversal. That said, as a mental refresher, I thought it would be worth exploring this use-case in Lucee CFML 5.3.6.61.

To see this scope traversal / scope look-up behavior in action, consider the following code in which we are performing Struct-iteration using the .each() member-method:

<cfscript>

	data = [
		"a": nullValue(),
		"b": "bee",
		"c": nullValue()
	];

	value = "The Spanish Inquisition";

	data.each(
		( key, value ) => {

			// CAUTION: When we reference VALUE here, ColdFusion is going to look for it
			// in a cascading number of places. First, the LOCAL scope. Then, the
			// ARGUMENTS scope. Then the VARIABLES scope. If VALUE is undefined in the
			// lower-level scopes, ColdFusion will continue to look for it higher-up in
			// the scope-chain.
			echo( key & " : " & ( value ?: "[undefined]" ) & " <br />" );

		}
	);

</cfscript>

As you can see, two of the keys in the data Struct reference undefined values. Now, if we iterate over this Struct and attempt to output the key-value pairs, we get the following browser output:

a : The Spanish Inquisition
b : bee
c : The Spanish Inquisition

Nobody expects the Spanish Inquisition! Instead, you probably expect [undefined] to show-up in the output. However, the argument name for our iteration value is value, which is also the name of a variable in the Variables scope. So, what's actually happening is that when we reference the unscoped variable value, ColdFusion is doing this:

  • Does value exist in the local scope? No, move on.
  • Does value exist in the arguments scope? No, move on.
  • Does value exist in the variables scope? Yes, use it.

Because of the scope look-up / cascading behavior in ColdFusion, our undefined value argument is skipped-over and we end up consuming the value defined on the variables scope.

To be fair, the Lucee documentation on Scope usage recommends that you explicitly scope all but the closest scope. In my demo, the closest scope is the local scope. As such, according to their recommendation, we should explicitly scope our arguments. Meaning, we should be referencing arguments.value, not value. And, if we make that change, we do get the expected output.

That said, adding explicit scopes to variable references is a personal preference. Of course, there is some performance benefit with not having to traverse the scope-chain. But, it all depends on what trade-offs you want to make in your code.

It could easily be argued that the true issue with the code is that I have both a local and global variable with the same name. In my opinion, this is a poor developer-choice as it creates ambiguity in reading of the code. Remove that ambiguity and you likely remove the unexpected behavior.

ASIDE: When I ran into this, I did some Googling and found that there is already a Lucee Developer Ticket that discusses this. It appears that if you enable full-null support in Lucee, the behavior may change.

Scope Cascading From a Security Point-of-View

Scope cascading does open-up an interesting security conversation because the url scope is towards the top of the cascade. Which means, a malicious actor could theoretically provide a "malicious fallback" value in the URL if they knew the name of an optional argument deep down in the call-stack.

Of course, this would require a "perfect storm" of insights and code constructs. But, it's not outside the realm of possibility. To see what I mean, let's look at a really simple UserService.cfc component that can create accounts with a given role:

component
	output = false
	hint = "I provide method for access user accounts."
	{

	public numeric function createUserAccount(
		required string name,
		required string email,
		required string password,
		string role
		) {

		var dbArguments = {
			name: name,
			email: email,
			password: password,

			// NOTE: If the role argument is undefined, we are going to fallback to a
			// standard user.
			// --
			// NOTE: If we provide a default value in the function signature, we would
			// not have any issues here.
			role: ( role ?: "user" )
		};

		systemOutput( serializeJson( dbArguments ), true, true );

	}

}

In this createUserAccount() method, the calling context can pass-in an optional role. And if they don't, we are going to fallback to using "user"via the Elvis operator.

Now, we can consume this method even when omitting the role argument:

<cfscript>

	userService = new UserService();

	userService.createUserAccount(
		name = "Ben",
		email = "ben@bennadel.com",
		password = "ourdeepestfearisnotthatweareinadequate"
	);

</cfscript>

If a user runs this page without any malicious intent, we get the following terminal output (formatted for readability):

{
	"role": "user",
	"password": "ourdeepestfearisnotthatweareinadequate",
	"name": "Ben",
	"email": "ben@bennadel.com"
}

As you can see, the role was defaulted to "user".

But, if a malicious user were to run this page with the following URL:

./url-test.cfm?role=ADMIN

... we would get the following terminal output (formatted for readability):

{
	"role": "ADMIN",
	"password": "ourdeepestfearisnotthatweareinadequate",
	"name": "Ben",
	"email": "ben@bennadel.com"
}

As you can see, because of the scope cascading behavior in ColdFusion, the malicious user was able to override the role argument with a URL-based value, "ADMIN".

To be clear, a lot of things have to go terribly wrong in order to expose a behavior like this. Critical values like role shouldn't be defaulted - they should always be explicitly defined in the business logic. And, if the argument signature had been changed to have a default value:

string role = "user"

... then we would have gotten the expected fallback, not the malicious value.

My point here isn't to strike fear into anyone's heart; or to try and convince everyone that they should immediately go explicitly-scope every reference (I don't do that). But, it's good to have this scope traversal behavior in the back of your mind so that you can pause and evaluate your coding decisions.

ColdFusion is a really dynamic language. This is part of what makes it such a joy to work with; and, really easy to get up and running. But, that dynamic behavior comes with a cost: both from a performance stand-point and from a mental-model stand-point. The point here is just to always be building-up a better understanding of how the runtime works. And, when something surprises you, stop and think about what it's doing mechanically; and, how you can bring that better-understanding with you into future decisions about application architecture.



Reader Comments

I guess I'm a little confused. I always thought member functions setup a closure. In fact, looking at the documentation for array.each() and struct.each() they both seem to back this up...

call the given UDF/Closure with every value in the array.
array.each( closure=function, parallel=boolean, maxThreads=number )

As such, I'd expect the key/value setup by the closure to both be locally scoped to their closure and that the closure wouldn't be able to access scopes outside of itself unless you passed them in explicitly. 🤔

Reply to this Comment

@Chris,

Actually, I believe a closure is designed for just precisely that: to provide you with access to the scope of its outer function.

Reply to this Comment

@Chris, @Andrew,

Exactly - the closure is just about bindings to the lexical scope - ie, the scope in which the closure was defined (not executed). The issue here is a closure doesn't remove the scope-traversal that the ColdFusion runtime does when evaluating variables. All it does is adjust which scopes are traversed.

Reply to this Comment

@Chris,

What would be great if ColdFusion just had first-class support for null -- then a lot of these issues go away (well, at least this one) :D I know Lucee CFML has a setting that you can turn on, but I have not tried it yet - I do think that would change how this works (but can't confirm).

Reply to this Comment

@Ben,

I'd waited so long for full NULL support in CFML but even though Lucee has now had it for some time now, I've been reluctant to implement it out of fear!!

For years I've coded around and against these CFML oddities caused by a lack of NULL support, so that now I fear a whole new batch of oddities will crop up on these VERY large code bases once implemented. Especially with all of the integration with 3rd party frameworks and module code.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.