Session token rotation REVISITED - Security Series #12.3.3 and #6.4.3

I posted on Friday about my experimental code for session token rotation and I got some great comments (thanks Peter and Brian). Brian stated in his comment that because I am using a <cflocation>, which is a 302 HTTP redirect, it could cause problems with legitimate deep-linking, plus, using <cflocation> feels like a hack. I agree with the latter. I was not happy with using <cflocation>, but it was all I could think to do at the time.

So I gave it some more thought this weekend and came up with a new way of doing it that uses <cfhttp> instead of a redirect. I am MUCH happier with this method for a couple of reasons.

  • It does not use a redirect, which makes it feel more stable. When using <cflocation> and <cfheader> tags, there were a couple of times when I caused infinite redirects while testing (oops).
  • This method feels cleaner to me, and I think can more easily be moved into a security object.

I do not know if this will address Brian's concerns about legitimate deep-linking, but this method does return a status code of 200 instead of a 302.

Session Rotation with CFHTTP


<cfcomponent output="false">
    <cfscript>
        this.name="SIDRotate";
        this.sessionManagement = true;
        this.sessionTImeout = createTimespan(0,0,0,30);
        this.setClientCookies = false;
    
</cfscript>

    <cffunction name="onSessionStart" access="public" output="false">
    <!--- Set a variable to store the cookies returned from the CFHTTP --->
    <cfset var cookieStruct = "" />

    <!---
    onSessionStart() only fires if no valid session tokens are provided. If it is fired and sessionReinit is not provided,
    it means the user is visiting for the first time, so all token cookies need to be expired
    --->

    <cfif NOT StructKeyExists(FORM, "sessionReinit")>

        <cfheader name="Set-Cookie" value="CFID=;expires=now" />
        <cfheader name="Set-Cookie" value="CFTOKEN=;expires=now" />

        <!---
        After token cookies are expired, we make a request back to the application, since there are no cookies, onSessionStart() will
        fire again for this request and provide cookies because the sessionReinit param will be provided so the <cfelse> block below will be executed
        --->

        <cfhttp url="http://#cgi.http_host#index.cfm" method="post" result="httpResult">
            <cfhttpparam type="formfield" name="sessionReinit" value="true" />
        </cfhttp>

        <!--- The cfhttp response will include a struct of cookies --->
        <cfset cookieStruct = httpResult.ResponseHeader['set-cookie'] />
            
        <!--- Loop over the cookieStruct and create new cookies --->
        <cfloop collection="#cookieStruct#" item="httpItem">
            <cfheader name="Set-Cookie" value="#StructFind(cookieStruct, httpItem)#" />
        </cfloop>

    <cfelse>
        <!---
        This cfelse block should only execute if sessionReinit param is provided, which means it should only execute for the cfhttp request.
        It will set cookies for each session tokens which will get returned in the response and used in the <cfif> block above,
        --->

        <cfheader name="Set-Cookie" value="CFID=#session.cfid#" />
        <cfheader name="Set-Cookie" value="CFTOKEN=#session.cftoken#" />
    </cfif>
        
    </cffunction>

</cfcomponent>

I've added comments this time around so the code may seem more verbose.

To start, I am still turning session management on and telling CF not to set cookies (in the pseudo-constructor setClientCookie=false).

Then in onSessionStart() I start by creating a variable to hold the cookies that will be returned from my <cfhttp> request.

The next step checks to see if the parameter "sessionReinit" exists. If it does not exist, then it means that this is the user's first time to the page since their last session expired. If this is the case, then this block will immediately expire any session token cookies and then make an <cfhttp> request back to the index.cfm page of the application (though technically it could be any page). Since we expired all of the cookies, the <cfhttp> request will not send any, so when the request gets to the application, onSessionStart() will fire again, this time with the sessionReinit parameter included, which we'll look at in a moment, but essentially, it tells the onSessionStart() method to set the cookies for the session tokens. Then the cookies will be returned in the response message.

When the response message is returned from the <cfhttp> call, any cookies that are returned will be set into the cookieStruct variable we set up at the top of the method.

We loop over the struct and set a cookie for each one returned. For this we are using the <cfheader> tag, because the values returned are in a format that can only set cookies using <cfheader>. Even if you did feel like parsing through the results so that you can use <cfcookie> , go for it.

Now let's look at the <cfelse> block for the check for the sessionReinit parameter. If sessionReinit does exist, then this block is run.


    <cfheader name="Set-Cookie" value="CFID=#session.cfid#" />
    <cfheader name="Set-Cookie" value="CFTOKEN=#session.cftoken#" />

This block simply sets the cookies from the session (which will be our new session tokens, into cookies so that they can be returned in the <cfhttp> response.

Conclusion

Again, this code is experimental. I do not know if there are underlying problems with it that will prevent its use. I can tell you that it probably should be the first thing in the onSessionStart() method and that you need to consider that session variables you set in here might be set twice, once for each of the sessions we are dealing with. To ensure that session variables are only set for the new session, they should be set in the <cfelse> block.

JEE Session Management and JSESSIONID tokens

Having two different forms of session management in CFML is really starting to get on my nerves. Everything needs to be tested twice and nothing works the same between the two. Each has their own pros and cons and neither seems to be blatantly better. I am going to try to do a comparison post in the future.

This method of session rotation does not seem to work with JEE sessions. I am going to have to look at some other options for rotating JSESSIONID. There is good news on the JSESSIONID front concerning session token rotation. In many cases, it will happen for you.

If your CFML application is deployed on a servlet container that times out sessions after a certain period of time, then the session tokens will be rotated for you. Even if previously valid session tokens are provided to ColdFusion, the container has invalidated the session, so it will provide a new one. I tested this on Tomcat 6.0 and it worked as expected. For some reason it does not work with the default JRUN install for CF8. I recommend you try this out on your JEE installation and see what happens.

Here is what I observed with Tomcat 6.0 and CF8:

  • In my Application.cfc I set the sessionTimeout to be 20 minutes
  • In my Tomcat web.xml file I set to 30 minutes

Everything worked as expected for the application's session time out. After 20 minutes the session timed out. If I refreshed the application in that 10 minute window between the application's session timeout and the containers session timeout, then the same JSESSION ID was used. But if I refreshed the application AFTER the container session timeout, then I got a new session token.

I will do more research on this in the future and write a more detailed post. If anyone as further info on this, please share.

Comments
Ben Nadel's Gravatar @Jason,

Very interest posts (this and the last). You've got me thinking about some stuff that I'll try to post. Out of curiosity, how come you use CFHeader to set the cooke and not CFCookie?
# Posted By Ben Nadel | 6/29/09 10:56 AM
Jason Dean's Gravatar @Ben,

<cfcookie> does not support the HTTPOnly attribute. It is a best practice to set session token cookies to be HTTPOnly so they cannot be compromised through the use of XSS attacks. I did not do that for these examples for the sake of clarity, but it probably would have been a good idea.

You can read more about it here: http://www.12robots.com/index.cfm/2009/1/5/mmmmMMm...
# Posted By Jason Dean | 6/29/09 11:48 AM
Shelby Spencer's Gravatar I have approached this problem a little differently. The following cfc will preserve the current Session values into the new session token, and behave much like SessionRotate in ColdFusion 10:

<cfcomponent>
   <cfset this.timeout = 30 /> <!---Session Rotate Timeout in Seconds --->
   
   <!--- Init should be called at the top of onRequestStart in Application.cfc --->
   <cffunction name="Init" access="public">
      <cfif NOT StructKeyExists(SERVER, "RotateTokens")>
         <cflock scope="Server" timeout="5" type="exclusive">
            <cfset SERVER.RotateTokens = {} />
         </cflock>
      <cfelse>
         <cfset HandleTimeOut() />
      </cfif>
   
      <cfif StructKeyExists(COOKIE, "RotateToken") AND StructKeyExists(SERVER.RotateTokens, COOKIE.RotateToken)>
         <cfset StructClear(SESSION) />
         <cfset StructAppend(SESSION, ObjectLoad(ToBinary(SERVER.RotateTokens[COOKIE.RotateToken].sessiondata)), True) />
         
         <cflock scope="Server" timeout="5" type="exclusive">
            <cfset StructDelete(SERVER.RotateTokens, COOKIE.RotateToken) />
         </cflock>
         
         <cfcookie name="RotateToken" expires="now" />
         <cfset StructDelete(COOKIE, RotateToken) />
      </cfif>
   </cffunction>
   
   <!---
      Should be called at the end of a login sequence to cause a session rotation.
      This will preserve all values in the current SESSION, in the new SESSION.
      NOTE: an error will be thrown if any code attempts to reference the SESSION scope after this call on a page.
   --->   
   <cffunction name="SessionRotate" access="public">
      <cfset StructDelete(SESSION, "SESSIONID") />
      <cfset StructDelete(SESSION, "URLtoken") />
      
      <cfset LOCAL.RotateToken = Hash(CreateUUID()) />
      
      <cflock scope="Server" timeout="5" type="exclusive">
         <cfloop condition="StructKeyExists(SERVER.RotateTokens, LOCAL.RotateToken)">
            <cfset LOCAL.RotateToken = Hash(CreateUUID()) />
         </cfloop>
         
         <cfset SERVER.RotateTokens[LOCAL.RotateToken] = {sessiondata=ToBase64(ObjectSave(SESSION)), timestamp=Now()} />
      </cflock>
      <cfset Cookie.RotateToken = LOCAL.RotateToken />
   
      <cfset SessionInvalidate() />
   </cffunction>
   
   <!---
      Can be called directly to invalidate the current token such as at the end of a logout sequence.
      NOTE: an error will be thrown if any code attempts to reference the SESSION scope after this call on a page.
   --->
   <cffunction name="SessionInvalidate" access="public">
      <cfcookie name="jsessionid" expires="now" />
      <cfset getPageContext().getSession().invalidate()>
   </cffunction>
   
   <cffunction name="HandleTimeOut" access="private">
      <cfset LOCAL.currentTime = Now() />
      <cfset LOCAL.keys = StructKeyArray(SERVER.RotateTokens) />
      
      <cfloop array="#LOCAL.keys#" index="key">
         <cfif DateDiff("s", SERVER.RotateTokens[key].timestamp, LOCAL.currentTime) GT this.timeout>
            <cflock scope="Server" timeout="5" type="exclusive">
               <cfset StructDelete(SERVER.RotateTokens, key) />
            </cflock>
         </cfif>
      </cfloop>
   </cffunction>
   
   <cffunction name="DeleteAllRotateTokens" access="private">
      <cflock scope="Server" timeout="5" type="exclusive">
         <cfset StructDelete(SERVER, "RotateTokens") />
      </cflock>
   </cffunction>
</cfcomponent>
# Posted By Shelby Spencer | 11/26/13 3:36 PM
Jim Davis's Gravatar Thanks Shelby! This has worked well for me.
# Posted By Jim Davis | 12/18/14 1:48 PM
Jim Davis's Gravatar Update - this solution fails in a clustered server environment.
# Posted By Jim Davis | 12/18/14 5:10 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner