Saturday 20 April 2013

It's easy to create a security hole in your application with onCfcRequest()

G'day:
Man... PHP continues to take a back seat with me, as interesting ColdFusion stuff keeps cropping up. Well: my definition of "interesting", anyhow.

Today on Stackoverflow I was reminded of a potential "gotcha" that onCfcRequest() brings to the table. One that has bitten me on the bum in the past, has done so with the person on S/O too, and has probably bitten other people too. And the "gotcha" creates quite a gaping security hole in the application if one doesn't deal with it.


I've not seen much coverage of this, so I'll detail it here.

First some background.

When Application.cfc was implemented back in CFMX7 (?), one of the methods added was a request interceptor, onRequest(). Instead of the requested file being run, onRequest() was run instead. This gives one's application a chance to do [something] regarding the requested template before, after and/or instead of running the template. An example would be this:

// Application.cfc
component {

    public void function onRequest(required string requestedFile){
        writelog(file="requests",text=arguments.requestedFile);
        if (arguments.requestedFile does not contain "restricted"){
            include arguments.requestedFile;
            writelog(file="requests", text="#arguments.requestedFile# Completed OK");
        }else{
            writelog(file="requests", text="#arguments.requestedFile# Blocked", type="warning");
            throw(type="InvalidFileException");
        }
    }
    
}

<!---publicFile.cfm --->
This is the public file

<!--- restrictedFile.cfm --->
Should not be able to access this!

If I browse to each of publicFile.cfm and restrictedFile.cfm, I get the expected results on the screen, and this in the log:

/shared/git/blogExamples/onRequest/publicFile.cfm
/shared/git/blogExamples/onRequest/publicFile.cfm Completed OK
/shared/git/blogExamples/onRequest/restrictedFile.cfm
/shared/git/blogExamples/onRequest/restrictedFile.cfm Blocked

So that's quite cool.

However note how the requested file is included. This approach won't be suitable for remote method requests, so for those, there is a method onCfcRequest(). This works the same way, but invokes the CFC & method, passing in the specified arguments:

public any function onCfcRequest(required string cfc, required string method, required struct args){
    return invoke(cfc, method, args);
}

(Obviously the same logic as per the onRequest() method could be used here too, but I've omitted it for the sake af clarity).

OK, great. What's the problem?

Well consider this CFC:

component {

    remote string function remoteMethod(){
        return "From remoteMethod()";
    }

    public string function publicMethod(){
        return "From publicMethod()";
    }

    package string function packageMethod(){
        return "From packageMethod()";
    }

    private string function privateMethod(){
        return "From rivateMethod()";
    }

}

Which methods are intended to be called here? Just the remote one. But here's the thing... a method call is only "remote" if it's being called from outwith the application. And when using onCfcRequest(), where is the method actually being called from? From this statement:

return invoke(cfc, method, args);

Which is inside the application. It's not registered as a remote call, because it's not being called remotely! This means that as well as all remote methods, one can also actually access all public methods too (package and private ones are still secure). This is something people don't necessarily realise, so one might inadvertently expose a helluva lot more of one's API than is the intent. YIKES.

So what's the mitigation here? Well one needs to do something similar to the logic we employed in onRequest() to block restricted files, but the problem is tricky to work around than that. One needs to do this:

public any function onCfcRequest(required string cfc, required string method, required struct args){
    var o = createObject(arguments.cfc);
    var metadata = getMetadata(o[method]); 
    
    if (structKeyExists(metadata, "access") && metadata.access == "remote"){
        return invoke(o, method, args);
    }else{
        throw(type="InvalidMethodException", message="Invalid method called", detail="The method #method# does not exists or is inaccessible remotely");
    }
}

Basically one needs to look at the object's metadata and check to see if the method being called is remote. If so: all good; if not: block it.

So make sure you do this in your onCfcRequest(). Otherwise you could be leaving yourself a bit open.

--
Adam