I was over on Peter Bell's blog talking about my philosophy of building applications without any sort of mapped paths when he raised an excellent point about security:
I'd have to spend some time playing with different names, but I'd probably eventually hit on the name of one of your cfcs. I honestly don't know whether I'd get anything of use in terms of maybe a file with config info or a DSN password or something and I don't even know how CF would handle the direct request for the cfc, so it may well not be an issue other than maybe allowing be to determine a couple of the valid cfc names you use, but I just like to keep my files away from such probing just in case!
All of my CFC are technically available for public access. My Application.cfc doesn't allow direct access nor do any of my methods (via the access="remote" value), but that's just a minor detail. The reason I do it this way is because, as anyone who knows about creating ColdFusion components, you will know that you cannot use parent directory paths such as "../../../cfc/Girl" when using the CreateObject() method. If you try that, you get the following error:
Component names cannot start or end with dots.
And, since I have no mapped paths in my applications, I needed the components to be lower than the www root so that I could access them using a proper dotted path. But, now that Peter has thrown down the gauntlet, it's time to prove that components can be moved outside the web root AND still be called without mapped paths.
The secret to doing this is way in which ColdFusion treats file location relationships. As you know, when you import a custom tag, call a CFModule, or include another file, ColdFusion treats the given path as being relative to the Current template path, NOT the Base template path. As it turns out, this is how it treats the paths to ColdFusion components as well - the path provided to the CreateObject() method is relative to the CURRENT template path, NOT the base template path.
So how do we leverage this information? We combine the fact that CFInclude uses relative paths with the fact that CreateObject() can use sub-directory dotted paths. First, let's talk about the directory structure:
In this example, our web root will be the "www" directory. Anything above or parallel to www folder is NOT accessible via a web url. The www_cfc directory will hold our ColdFusion components in a secure, non-web-accessible location. Ideally what we want to be able to instantiate CFCs in the www_cfc directory FROM the www directory without using a mapped path.
To do this, we need to create a proxy for creating CFCs; we need to have a middle man that has the ability to create CFCs using non-mapped paths that we can call from within our application. This middle man is the following function:
<cffunction name="CreateCFC" access="public" returntype="any" output="false" hint="Creates a CFC Creation proxy. Does NOT initialize the component, only creates it."> <!--- Define arguments. ---> <cfargument name="Path" type="string" required="true" /> <!--- Return the created component. ---> <cfreturn CreateObject( "component", ("www_cfc." & ARGUMENTS.Path) ) /> </cffunction>
All this function does is create a component at the passed in Path. As you can see, the ColdFusion component that is created uses the path starting with "www_cfc". To do this, this function must be located above the www_cfc directory, and that is exactly how it works. Let's save this method in an ColdFusion template above both the www and the www_cfc directories:
Here, we have created the create_cfc.udf.cfm file. This file contains nothing BUT the method declaration. Now, remember, since ColdFusion treats component paths as relative to the current template, this file, create_cfc.udf.cfc, should be able to instantiate any component in any of the sub directories www or www_cfc.
But, this method is outside the web root. It doesn't matter. We don't want to call it from a URL; we just want to be able to access it. And how do we access it? Through the flexibility of ColdFusion CFInclude tags and the pre-processing nature of the Application.cfc.
Let's look at our Application.cfc code (located IN the www directory):
<cfcomponent displayname="Application" output="true" hint="I do pre-page processing for the application"> <!--- Run the pseudo constructor to set up default data structures. ---> <cfscript> // Set up the application. THIS.Name = "CFC_TEST"; THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 5, 0 ); THIS.SessionManagement = false; THIS.SetClientCookies = false; </cfscript> <!--- Include the CFC creation proxy. ---> <cfinclude template="../create_cfc.udf.cfm" /> <cffunction name="OnRequestStart" access="public" returntype="boolean" output="true" hint="I do pre-page processing for the page request."> <!--- Store the CreateCFC method in the application scope. We *wouldn't* do this for every page request... this is JUST an example. ---> <cfset APPLICATION.CreateCFC = THIS.CreateCFC /> <cfreturn true /> </cffunction> </cfcomponent>
As you can see, see, as part of the Application.cfc pseudo constructor code (the code outside of method declarations), we are including the create_cfc.udf.cfm template from the parent directory. CFInclude can use relative paths so going up one directory for CFInclude is no problem at all. This overlaps with the idea of CFInclude-based Mixins that I have explored briefly in other posts.
The CFInclude tag pulls in the method declaration CreateCFC() and stores it in the THIS scope of the Application.cfc instance. Then, in the OnRequestStart() method, we are storing the method pointer into the APPLICATION scope. This makes this method APPLICATION-scoped and therefore publicly accessible to the application framework. Now, keep in mind, this is JUST for an example. I would never store the method repeatedly for EVERY page call. This method pointer would obviously be cached during Application initialization.
Let's take a quick look at the Girl.cfc:
<cfcomponent displayname="Girl" extends="AbstractBaseComponent" output="no" hint="I am a girl object."> <!--- Run the pseudo constructor to set up default data structures. ---> <cfscript> // Set up default public properties. THIS.FirstName = ""; THIS.LastName = ""; THIS.ValidPickupLines = ""; </cfscript> <cffunction name="Init" access="public" returntype="Girl" output="false" hint="Returns an initialized girl instance."> <!--- Define arguments. ---> <cfargument name="FirstName" type="string" required="false" default="" /> <cfargument name="LastName" type="string" required="false" default="" /> <cfargument name="PickupLines" type="array" required="false" default="#ArrayNew( 1 )#" /> <!--- Store arguments. ---> <cfset THIS.FirstName = ARGUMENTS.FirstName /> <cfset THIS.LastName = ARGUMENTS.LastName /> <cfset THIS.ValidPickupLines = ARGUMENTS.PickupLines /> <!--- Return This reference. ---> <cfreturn THIS /> </cffunction> <cffunction name="TryPickupLine" access="public" returntype="string" output="false" hint="This tries a pickup line on the Girl."> <!--- Define arguments. ---> <cfargument name="Line" type="string" required="true" /> <!--- Check to see if the pickup line is valid. ---> <cfif (THIS.ValidPickupLines.IndexOf( JavaCast( "string", ARGUMENTS.Line ) ) GTE 0)> <!--- This was a valid pickup line. ---> <cfreturn ( "Hey, my name is " & THIS.FirstName & "." & " Why don't you buy me a drink?" ) /> <cfelse> <!--- This was NOT a valid pickup line. ---> <cfreturn ( "Does that line usually work with a woman? " & "Maybe you should try something else." ) /> </cfif> </cffunction> </cfcomponent>
Now that we have the method APPLICATION-scoped, and the definition for the Girl.cfc, let's see how we can use it:
<!--- Create a set of pickup lines. ---> <cfset arrLines = ListToArray( "Hey baby, what's your sign?|" & "Excuse me miss, I think you are a hottie!|" & "Whoa! Where have you been all my life?!?", "|" ) /> <!--- Create a Girl object. Notice that we are not using ColdFusion's CreateObject() method directly. Instead we are going through the proxy method that calls it for us. We then, initialize the returned object just as we would for the standard CreateObject() method call. ---> <cfset Girl = APPLICATION.CreateCFC( "Girl" ).Init( FirstName = "Libby", LastName = "Smith", PickupLines = arrLines ) /> <!--- Try to pick up the girl. ---> <p> #Girl.TryPickupLine( "Can I buy you a drink?" )# </p> <p> #Girl.TryPickupLine( "You look hot in that dress!" )# </p> <p> #Girl.TryPickupLine( "Whoa! Where have you been all my life?!?" )# </p>
This gives us the output:
Does that line usually work with a woman? Maybe you should try something else.
Does that line usually work with a woman? Maybe you should try something else.
Hey, my name is Libby. Why don't you buy me a drink?
That there, my friends, is a proof of concept that ColdFusion components can be instantiated from sub-directories without using any sort of mapped paths. And what did it take? Just one ColdFusion template and a few more lines of code in the Application.cfc. But, just to go over the example once, you will see that when I create the CFC, Girl, I am calling the APPLICATION-scoped method, CreateCFC(). This, method, while it exists in the Application.cfc and is scope to the APPLICATION scope, was defined outside of the web root. And, since ColdFusion treats component paths relative to the DEFINING template, NOT the CALLING template, everything just falls into place.
And, if you encapsulate the way objects are created anyway, such as through some sort of service or factory, this concept should be totally transparent. The majority of your application will not have to know about it at all.
Want to use code from this post? Check out the license.