Unexpected Scope Precedence When CFIncluding Template Into A ColdFusion Component
For the last week, I've been trying to track down an inconsistent email bug at InVision. It was one of those bugs that only seemed to happen in production; and, it happened inconsistently at that (probably due to the load balancers). After deploying several augmented debugging and logging features, I believe that I narrowed the problem down to a var-leak in one of the email templates. But, while there was a variable declaration problem, the issue seems to be further exacerbated by an unexpected scope precedence issue within templates that are included into a ColdFusion component.
Normally, in the context of a ColdFusion component, the Local function scope (ie, where you "var" variables) takes precedence over the private / global Variables scope when it comes to variable access. Meaning, if you read from or write to an unscoped variable, ColdFusion will attempt to locate that variable in the Local scope before it, eventually, looks in the Variables scope.
Now, ColdFusion components are extremely dynamic. Not only can you CFInclude Functions into a component, you can CFInclude entire components within of other components, creating Ruby-inspired mixins. And, for the most part, this works (although it's not a terribly common practice). But, it seems that if you include a CFML template inside of a ColdFusion component, the scope precedence gets a bit messed up and the Variables scope will take a higher precedence than the Local scope.
NOTE: I tested this up to and including ColdFusion 11 and it consistently breaks.
To see this in action, I've created a simple ColdFusion component that has a few different test methods that all revolve around accessing a variable called "value."
component {
public void function leak() {
// CAUTION: Because I am leaving this value unscoped, it will store the value
// into the Variables scope (ie, the private scope of the component).
value = "Variables scope";
}
public void function test() {
include "./test-scope.cfm";
}
public void function testBoth() {
var value = "Var'd in function before used in CFInclude.";
include "./test-scope.cfm";
}
public void function testInline() {
local.value = "Override in local";
// NOTE: We are not providing an explicit scope.
value &= " ( with concatenation )";
// NOTE: We are not providing an explicit scope.
writeOutput( "Unscoped read: #value# <br />- - -<br />" );
// Now that we've executed our varaible access, let's read from explicit scopes
// to see what the read and write precedence is.
if ( structKeyExists( variables, "value" ) ) {
writeOutput( "Variables: #variables.value# <br />" );
}
if ( structKeyExists( local, "value" ) ) {
writeOutput( "Local: #local.value# <br />" );
}
}
}
Notice that one of the methods - leak() - writes to an unscoped "value" variable. Generally speaking, in ColdFusion, when you write to an unscoped variable, it gets stored in the Variables scope unless it is already defined in a higher-precedence scope (such as the local scope or arguments scope).
Also take notice that the test() function CFIncludes a template. In this case, the content of that template is the same as the body of the testInline() method (our control method); but, I'll reproduce it here as well:
<cfscript>
local.value = "Override in local";
// NOTE: We are not providing an explicit scope.
value &= " ( with concatenation )";
// NOTE: We are not providing an explicit scope.
writeOutput( "Unscoped read: #value# <br />- - -<br />" );
// Now that we've executed our varaible access, let's read from explicit scopes
// to see what the read and write precedence is.
if ( structKeyExists( variables, "value" ) ) {
writeOutput( "Variables: #variables.value# <br />" );
}
if ( structKeyExists( local, "value" ) ) {
writeOutput( "Local: #local.value# <br />" );
}
</cfscript>
As you can see, in the included template, we're attempting to write a variable to the Local scope and then read that variable without a scope. The use of the string concatenation is to see how reads and writes interplay. I believe that the expected behavior is that the Local scope should have both read and write precedence; and, in the context of a ColdFusion component, it does. But, in the context of the CFInclude, I will show you that it does not.
First, let's try running the test() method alone, without any variable leak at all:
<cfscript>
tester = new ScopeTester();
// First, let's test the CFInclude without any leaks.
tester.test();
</cfscript>
When we run this, we get the following output:
Unscoped read: Override in local ( with concatenation )
- - -
Variables: Override in local ( with concatenation )
Local: Override in local
Something really strange is going on here. Without a variable leak, you can see that the value still ends up in the Variables scope. In the string concatenation, ColdFusion appears to be reading from the Local scope but then writing the Variable scope. Now that I think about it, this is the same kind of asymmetric access that we see in unscoped variables inside of a CFThread block.
So, already, we're seeing that using unscoped "local" variables in a CFInclude is problematic. But, let's see what happens when we introduce our variable leak:
<cfscript>
tester = new ScopeTester();
// Next, let's test the CFInclude with an existing leak.
tester.leak();
tester.test();
</cfscript>
This time, we're letting the "value" leak into the Variables scope before we call our test method. And, when we run the above code, we get the following output:
Unscoped read: Variables scope ( with concatenation )
- - -
Variables: Variables scope ( with concatenation )
Local: Override in local
This basically confirms what we saw in the first test - ColdFusion is giving the Variables scope a higher read and write precedence, compared to the Local scope, in the context of the CFInclude.
Now, you could work around this issue by explicitly scoping every variable reference inside of your CFInclude. But, that feels like an "old-school Ben" move. That's not how I roll these days. Luckily, there's an easier work-around, which is simply to ensure that you var the variables inside of the Function before they are referenced within the CFInclude. To test this, we'll use the testBoth() method, which simply var's the value first:
<cfscript>
tester = new ScopeTester();
// Next, let's test the CFInclude with an existing leak, but with the value
// variable VAR'd in the ColdFusion component before it is used in the include.
tester.leak();
tester.testBoth();
</cfscript>
This time, when we run the code, we get the following output:
Unscoped read: Override in local ( with concatenation )
- - -
Variables: Variables scope
Local: Override in local ( with concatenation )
Ah, much better! We can still see the leaked value; but, with the value var'd in the Function first, the usage within the CFInclude works as "expected."
To be honest, I am not sure why the "work around" works any differently. Meaning, I am not sure how or why the very existence of the "var" keyword seems to give the Local scope the appropriate precedence within the CFInclude. Very strange.
Want to use code from this post? Check out the license.
Reader Comments
Shades of the unscoped scope in the very first CFMX (6.0)!!
@WebManWalking,
Ha ha, I barely remember CFMX 6. Side-story, I knew a guy who was on CF5 forever, like even after CF8 was out. He swore it was the most stable version of ColdFusion :)