Custom CFParam Tag That Exposes Error Information In Lucee CFML 5.3.7.48
Over on the Lucee CFML developer forum, there was a discussion about accessing cfparam
error information in the event that a type-casting error occurs. Currently, you would have to do some fancy string-parsing to get this data out of the given error object. Since I've been heads-down in ColdFusion custom tags lately, I thought it would be a fun code kata to create a ColdFusion custom tag that wrapped the cfparam
tag and then exposed exactly this type of information in the event of a parameterization error in Lucee CFML 5.3.7.48.
ASIDE: This is not the first time I've considered wrapping the
cfparam
tag. In fact, I first tried this 14 years ago (in 2007) when exploring the idea of adding acatch
attribute to thecfparam
tag.
When you throw an error object in ColdFusion, all of the error object attributes are strings. Unless you create a custom Java Exception object, which feels like a heavy-lift for this exploration. As such, the error details that I provide in this code kata are going to be serialized JSON (JavaScript Object Notation) values stuffed into the detail
and extendedInfo
string attributes.
detail
- I will contain the serializederror
object thrown by the underlyingcfparam
tag.extendedInfo
- I will contain the serialized error information that provides clearer insights as to why the aforementioned error was thrown.
Essentially, my ColdFusion custom tag is not much more than a glorified try/catch
block that wraps the native cfparam
tag. I'm calling this ColdFusion custom tag, specify.cfm
, because naming stuff is hard:
<cfscript>
param name="attributes.name" type="string";
param name="attributes.type" type="string";
// NOTE: The "default" attribute is OPTIONAL.
try {
// Translate parameter reference into a CALLER-scoped reference so that we will
// check and mutate the calling context, not the current ColdFusion custom tag
// context.
callerReference = "caller.#attributes.name#";
param
name = callerReference
type = attributes.type
default = ( attributes.default ?: nullValue() )
;
} catch ( any error ) {
// The Detail attribute will contain the underlying error object, with the
// "caller." stripped-out. This should help it read as though the error happened
// in the calling context, not in the ColdFusion custom tag context.
detailString = serializeJson( error )
.replace( "caller.", "", "all" )
;
// The ExtendedInfo attribute will contain details about the parameter that is
// being specified, including whether or not the reference was already defined.
extendedInfo = attributes.copy();
extendedInfo.isDefined = isDefined( callerReference );
extendedInfo.definedValue = ( extendedInfo.isDefined )
? getVariable( callerReference )
: nullValue()
;
extendedInfo.hasDefault = attributes.keyExists( "default" );
extendedInfoString = serializeJson( extendedInfo );
throw(
type = "SpecifyFailure",
message = "Underlying cfparam failed within cf_specify.",
detail = detailString,
extendedInfo = extendedInfoString
);
}
exit method = "exitTag";
</cfscript>
As you can see, this ColdFusion custom tag is really just a glorified try/catch
block that takes the given error information and exposes it in a more "structured" way in the subsequently-throw error.
Now, we can consume cf_specify
in essentially the same way that we would have consumed the cfparam
tag:
<cfscript>
// Explicitly build up the struct.
cf_specify( name = "url.foo", type = "struct", default = {} );
cf_specify( name = "url.foo.bar", type = "struct", default = {} );
cf_specify( name = "url.foo.bar.baz", type = "string", default = "Explicit struct creation!" );
// Implicitly build up the struct (ie, have ColdFusion create the parent structs as
// needed when param'ing the variable).
cf_specify( name = "url.hello.world", type = "string", default = "Implicit struct creation!" );
// Try with some array values.
url.values = [];
cf_specify( name = "url.values[ 1 ]", type = "string", default = "Array value 1 creation!" );
cf_specify( name = "url.values[ 2 ]", type = "string", default = "Array value 2 creation!" );
cf_specify( name = "url.values[ 3 ]", type = "string", default = "Array value 3 creation!" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Make sure the default-less calls work.
cf_specify( name = "url.foo.bar", type = "struct" );
cf_specify( name = "url.hello.world", type = "string" );
try {
// This one should fail (can't cast String to Boolean):
cf_specify( name = "url.values[ 2 ]", type = "boolean" );
// ... as such, we should never hit this throw.
throw( type = "WeShouldNeverGetThisFar" );
} catch ( "WeShouldNeverGetThisFar" error ) {
dump( error );
abort;
} catch ( any error ) {
// Swallow error in this example.
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
try {
// CAUTION: We know the "baz" value is a string and the TYPE here will fail.
cf_specify( name = "url.foo.bar.baz", type = "boolean" );
} catch ( "SpecifyFailure" error ) {
// The SpecifyFailure error contains JSON-serialized information about the root
// error, including what was asked and what was provided.
// Underlying CFParam error:
dump(
label = "Underlying CFParam error",
var = deserializeJson( cfcatch.detail ),
show = "type, message, detail"
);
// What was provided:
dump(
label = "CF_Specify information",
var = deserializeJson( cfcatch.extendedInfo )
);
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
dump(
label = "URL Scope",
var = url
);
</cfscript>
The top portion of this ColdFusion file is just exercises the cf_specify
tag; but, the bottom portion is where the magic happens - notice that in the catch
block of our error, I am deserializing and outputting the .detail
and .extendedInfo
properties of the caught error object. And, doing so gives us the following page output:
As you can see, we can easily access the following:
- The value we were param'ing already existed.
- The value we were param'ing had the existing value,
Explicit struct creation!
. - The value we were param'ing was a string.
- The value could not be cast to a
boolean
.
Given this clear error information, we can implement error handling techniques with better precision. For example, we could catch "hacking attempts" and block IP addresses. This type of error:
Can't cast String [j_security_check] to a value of type [numeric]
Could now be easily checked because I would quickly see two pieces of information:
extendedInfo.type
:numeric
extendedInfo.definedValue
:j_security_check
And, I could have a large switch
statement at the root of my error-handling that looks for type-casting errors with defined values and then blocks IP-addresses based on a known list of offenders like:
j_security_check
passwords.txt
file:
bxss
nslookup
@@
-1 OR
config.ini
Of course, you don't want to go too crazy or you might start blocking legitimate bugs in your code (that need to be fixed, not blocked).
Anyway, just a fun little ColdFusion code kata on a quiet Sunday morning.
Want to use code from this post? Check out the license.
Reader Comments
Awesome work. I ended up doing something similar to handle my cfparams better. I haven't rolled it out everywhere yet, but the spots where I have, it's been working a treat.
One thing you might need to add to make this work more broadly though is support for the pattern, min, max, maxlength attributes that cfparam also has.
@Peter,
That's awesome to hear that 1) this is on the right track and 2) it seems to work in a real world scenario. Thanks for the feedback!