Ask Ben: Securely Pinging One ColdFusion Application From Another
Hi Ben thanks for your wonderful advice. I have a question. I have a CMS where the admin part is going to be behind a firewall different domain than the public facing side. How can I refresh application objects on the public facing side from the admin side with different domains?
When two applications live under different domain names and on different servers potentially, the only way for them to communicate (that I have any experience with) is over an HTTP connection. If you want one application to refresh part of another application, it has to make some sort of a request over HTTP to trigger the refresh in the other application. The biggest problem with any HTTP connection is that anyone with a browser (or ninja-like programming skills) can access it in the same way the originating application could.
To be honest, I am not a security expert when it comes to web applications. I know how to really lock down a ColdFusion application internally; but, when it comes to things like secure HTTP calls, I'm a bit ignorant (I bet ColdFusion security expert Jason Dean would have more to say on this). What I do know is that not everyone has HTTPS capabilities on their server. As such, I am writing this post with the assumption that it can be useful for non-https connections.
What we are going to try to do here is create a secure "handshake" between the two servers; the originating server is going to be "making" the handshake and the receiving server is going to be "accepting" the handshake. Now, because we are making this request over a potentially non-secure HTTP call, we are going to be taking several steps to help ensure the security of the communicae. For example, both servers have to agree upon a private encryption key, encryption method, user agent, and referring url. Now, there is a lot of logic when things of this nature get involved; as such, I have tried my best to encapsulate all of this functionality into a single, easy to use ColdFusion component: HandShake.cfc.
The HandShake.cfc ColdFusion component defaults all of these values to make the execution quite simple. When you implement this CFC, however, you should change the default values from the various arguments to make sure public people are not privy to your handshake settings. Before we get into the guts of the HandShake.cfc, let's look at a sample use case.
Here is a test page on the originating server:
<!--- Create the handshake object. --->
<cfset objHandShake = CreateObject(
"component",
"HandShake"
).Init()
/>
<!---
Define the additional parameters that will be used on
the accepting server to help determine what service
to refresh.
--->
<cfset objParameters = {
Service = "Verity"
} />
<!--- Make the handshake. --->
<cfset strResult = objHandShake.MakeHandShake(
Url = "http://serverone.com/accept.cfm",
Parameters = objParameters
) />
<!--- Output the result. --->
<cfoutput>
Result: #strResult#
</cfoutput>
Not much going on here. We simply instantiate the HandShake.cfc ColdFusion component with default configuration values and then call MakeHandShake() passing in the target server URL and whatever additional parameters we want to send.
On the target server, which must have its own copy of the same HandShake.cfc, we are using the AcceptHandShake() method:
<!--- Create the handshake object. --->
<cfset objHandShake = CreateObject(
"component",
"HandShake"
).Init()
/>
<!---
Try to accept handshake. If the handshake is not
valid, it will throw various acceptions.
--->
<cftry>
<!--- Get the parameters. --->
<cfset objParameters = objHandShake.AcceptHandShake(
CGI = CGI
) />
<!--- Set echo value. --->
<cfset strEcho = objParameters.Service />
<!--- Catch errors. --->
<cfcatch>
<!--- The handshake was not valid. Echo back error. --->
<cfset strEcho = CFCATCH.Type />
</cfcatch>
</cftry>
<!--- Echo back value. --->
<cfcontent type="text/plain" reset="true" />
<cfoutput>#strEcho#</cfoutput>
If the hand shake is accepted (considered valid), the AcceptHandShake() method will return the parameters struct that the originating server transmitted along with the communicae. When we run the two pages above, we get the following output:
Result: Verity
Once the handshake is accepted, you can then go ahead and safely perform any logic that is inherent to the target page (ie. refreshing some service in the application).
Now, if any part of the hand shake is not valid, the AcceptHandShake() method will throw custom exceptions. That is why we have to wrap it in a Try/Catch block. If, for example, we instantiated the accepting HandShake.cfc with a custom useragent that was not used by the originating server:
<!--- Create the handshake object. --->
<cfset objHandShake = CreateObject(
"component",
"HandShake"
).Init( UserAgent = "firefox" )
/>
... we will get this output:
Result: HandShake.InvalidUserAgent
As you can see, the HandShake.cfc ColdFusion component is quite easy to use. All of the encryption, decryption, and communication logic is encapsulated behind the component's method calls. Now, let's take a look at how this is all taking place:
<cfcomponent
output="false"
hint="I provide a way to create more secure public pinging.">
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="I return an intialized object.">
<!--- Define arguments. --->
<cfargument
name="EncryptionKey"
type="string"
required="false"
default="b**bs@r3w!ck3dc**1"
hint="I am the encryption key used for both encrypting and decrypting the handshake."
/>
<cfargument
name="TimeToLive"
type="numeric"
required="false"
default="#CreateTimeSpan( 0, 0, 0, 5 )#"
hint="I am the time to live for the request (the time for which the request is valid)."
/>
<cfargument
name="Referer"
type="string"
required="false"
default="secure.handshake.com"
hint="I am an agreed upon site referrer that the handshake will check for."
/>
<cfargument
name="UserAgent"
type="string"
required="false"
default="bot.handshake.com"
hint="I am an agreed upon user agent that the handshake will check for."
/>
<cfargument
name="EncryptionMethod"
type="string"
required="false"
default="cfmx_compat"
hint="I am the encryption method to use."
/>
<!--- Store the configuration data. --->
<cfset VARIABLES.Instance = {
EncryptionKey = ARGUMENTS.EncryptionKey,
TimeToLive = ARGUMENTS.TimeToLive,
Referer = ARGUMENTS.Referer,
UserAgent = ARGUMENTS.UserAgent,
EncryptionMethod = ARGUMENTS.EncryptionMethod
} />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="AcceptHandShake"
access="public"
returntype="struct"
output="false"
hint="I accept and authenticate a handshake and return any paramters that were passed. If the request is not valid, I throw an exception.">
<!--- Define arguments. --->
<cfargument
name="CGI"
type="struct"
required="true"
hint="I am the CGI data struct for this system."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Check to see if the referer is valid. --->
<cfif (ARGUMENTS.CGI.http_referer NEQ VARIABLES.Instance.Referer)>
<!--- Throw error. --->
<cfthrow
type="HandShake.InvalidReferer"
message="The handshake referer is not valid."
detail="The referer used in this handshake, #ARGUMENTS.CGI.http_referer#, is not valid."
/>
</cfif>
<!--- Check to see if the user agent is valid. --->
<cfif (ARGUMENTS.CGI.http_user_agent NEQ VARIABLES.Instance.UserAgent)>
<!--- Throw error. --->
<cfthrow
type="HandShake.InvalidUserAgent"
message="The handshake user agent is not valid."
detail="The user agent used in this handshake, #ARGUMENTS.CGI.http_user_agent#, is not valid."
/>
</cfif>
<!--- Get the encrypted data. --->
<cfset LOCAL.EncryptedData = ARGUMENTS.CGI.query_string />
<!---
There are several ways for the rest of the data to
throw an error (decrypting and deserializtion, etc.).
Wrap the whole thing in a try/catch for custom error
handling.
--->
<cftry>
<!--- Decrypt the data. --->
<cfset LOCAL.DecryptedData = Decrypt(
LOCAL.EncryptedData,
VARIABLES.Instance.EncryptionKey,
VARIABLES.Instance.EncryptionMethod,
"hex"
) />
<!--- Remove the dirty text from around decryption. --->
<cfset LOCAL.JSONData = REReplace(
LOCAL.DecryptedData,
"^\d+>|<\d+$",
"",
"all"
) />
<!---
Now that we have the true JSON data, deseriarlize
it into a ColdFusion struct.
--->
<cfset LOCAL.RequestData = DeserializeJSON(
LOCAL.JSONData
) />
<!--- Check to see if the time to live is valid. --->
<cfif (LOCAL.RequestData.ValidUntil LT Now())>
<!--- Throw error. --->
<cfthrow type="HandShake.ValidUntil" />
</cfif>
<!---
ASSERT: If we have made it this far, then the
request has been deemed valid and secure from
the referring agent.
--->
<!--- Return the parameter data. --->
<cfreturn LOCAL.RequestData.Parameters />
<!--- Catch any errors. --->
<cfcatch>
<!--- Throw custom error. --->
<cfthrow
type="HandShake.ParsingError"
message="There was a problem parsing the requst."
detail="#CFCATCH.Type#"
/>
</cfcatch>
</cftry>
</cffunction>
<cffunction
name="MakeHandShake"
access="public"
returntype="string"
output="false"
hint="I make the handeshake using CFHTTP with the given configuration data and any additional URL parameters.">
<!--- Define arguments. --->
<cfargument
name="Url"
type="string"
required="true"
hint="I am the URL being called in the handshake."
/>
<cfargument
name="Parameters"
type="struct"
required="false"
default="#StructNew()#"
hint="I am the collection of additional parameters being sent."
/>
<cfargument
name="Username"
type="string"
required="false"
default=""
hint="I am the CFHTTP username."
/>
<cfargument
name="Password"
type="string"
required="false"
default=""
hint="I am the CFHTTP password."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!---
Build up the data that we are going to be sending
with our request. This will be an ordinary ColdFustion
struct that will be serialized into JSON and then sent
across as the encrypted query string.
--->
<cfset LOCAL.RequestData = {
ValidUntil = (Now() + VARIABLES.Instance.TimeToLive),
Parameters = ARGUMENTS.Parameters
} />
<!--- Serialize the request data and encrypt it. --->
<cfset LOCAL.EncryptedData = Encrypt(
(
RandRange( 1, 999999999 ) &
">" &
SerializeJSON( LOCAL.RequestData ) &
"<" &
RandRange( 1, 999999999 )
),
VARIABLES.Instance.EncryptionKey,
VARIABLES.Instance.EncryptionMethod,
"hex"
) />
<!--- Build up the CFHTTP attributes. --->
<cfset LOCAL.CFHTTPAttributes = {
URL = (ARGUMENTS.Url & "?" & LOCAL.EncryptedData),
Method = "get",
UserAgent = VARIABLES.Instance.UserAgent,
Username = ARGUMENTS.Username,
Password = ARGUMENTS.Password
} />
<!--- Make the HTTP request. --->
<cfhttp
attributecollection="#LOCAL.CFHTTPAttributes#"
result="LOCAL.HttpGet">
<!--- Set the referer. --->
<cfhttpparam
type="header"
name="referer"
value="#VARIABLES.Instance.Referer#"
/>
</cfhttp>
<!--- Return the resultant file content. --->
<cfreturn LOCAL.HttpGet.FileContent />
</cffunction>
</cfcomponent>
The encryption algorithm used in the HandShake.cfc ColdFusion component defaults to the built-in CFMX_COMPAT method. I know I get a lot of heat for using this as it is apparently weak, so you can override it if you want; however, realize that if you choose another encryption algorithm, the set of valid encryption key characters changes (so be careful when overriding both of these defaults).
As I said, I am not a security expert, but hopefully this can at least give you some inspiration or point you in a better direction.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben, a couple of quick questions. It appears that the CFC needs to live on both servers is that correct? Also, where does any processing code need to go in the CFC that is accepting the handshake?
@George,
That is correct - both servers need a copy of the CFC (or at least need to share the same central one (if possible)).
As far as processing code, it would go outside of the CFC itself. The CFC just handles the handshake authentication. Once that is done, you can put your logic after the CFC.
For example, on an "accept.cfm" style page, you could do something like:
<cftry>
... check handshake via AcceptHandShake() ....
... if we made it this far, it was accepted. Now do custom logic ....
<cfcatch>
....
</cfcatch>
</cftry>
As you can see, your custom logic would be after the AcceptHandShake() method executed without throwing an exception.
Ben, I think this looks like a very clever solution to the problem and for many situations, I think it would probably be just fine. I just wanted to note a few things:
1. This does not solve the problems that are generated by not using an SSL connection (equally, using an SSL connection will not solve the problems that your solution does). SSL will simply prevent the traffic from being sniffed and will verify the server to the requester, but as far as I know, it will cannot verify the requester to the server, as your handshake does. I believe there are browser certificates that can do things like that, but I don't know if that could be done programmatically.
But the point about SSL is that the traffic going between the two applications could still be intercepted and, if a weak encryption is used, decrypted.
2. One of the biggest issues to deal with when dealing with encryption is Key Management. Encryption Key management is a pain. In your solution, you needed to hard code an encryption key into your code and the key does not change. In a really secure application, this is frowned upon. Greatly.
Unfortunately, the solutions for key management are not easy. Having an unchanging key is bad, so it's suggested you use a temporal key, but then how do you keep the keys in sync between servers?
Also, having your keys hard-coded is bad, but then where do you put them? And how do you make that location secure?
I wish there were easy answers for this. I'm still learning a lot of this myself, and it feels over-whelming.
I just wanted to make the point that, while your solution is a good one, it cannot be considered highly secure. It is still better than no handshake and no encryption and for most small applications that don't need a high level of security, it should be just fine.
@Jason Dean: "SSL will simply prevent the traffic from being sniffed"
SSL does not do this. Performing a MIM attack against SSL traffic is fairly trivial - the WebScarab Java program does this in a point-n-drool cross platform GUI, for instance. You'd need to activly check certificate hashes to prevent it, and hardly anything does. And then you're reduced to the key exchange problem again to make sure both ends know the expected hash.
In short, security is hard :-)
@Tom, I have to somewhat disagree. SSL DOES prevent packet sniffing, and while it is true that a Man-In-the-Middle attack can be performed against an SSL connection, I'm not sure it should be called trivial. As far as I know, and I am not an expert on MITM attacks, it still requires getting a proxy in place to be the MITM or somehow poisoning a DNS to redirect traffic.
@Jason,
Yeah, I agree that the hard-coded key is not good. But as long as that is not stolen, especially for a small app, as you say, it should be ok. I was considering making some sort of time-based key, but then I realized I don't know any theory behind that :)
Ben, I think a webservice would make more sense in this case. You've basically recreated RPC over HTTP, which is exactly one of the solutions that Web Services provide.
Also, if you need serious security for an operation like this, it is worth looking into SAML and WS-Security.
@Roland,
In essence, what I did create was actually a web service.
The person in question should really look at Sean's article about using JMS messaging to do synchronization.
http://corfield.org/blog/index.cfm/do/blog.entry/entry/Transfer_Cache_Synchronization_in_a_Cluster
@Jason: Aye, there are additional steps.
There are, for instance, a depressing number of DNS servers vunerable to the Kamisnky bug or unpatched Cisco kit though...
As others mention, everything boils down to key exchange...
@Chris,
The JMS stuff looks interesting, but also considerably more moving parts. That seems like something that would / should be used with complex sites.
What if you happens in the case of appB extending appA and you want appB to set an application scoped variable in appA?
You can't call the appA by dot notation, appA.application.variable so how do you access that way without going through all the complexity detailed above? Or am I over simplifying it?