Philip Chalmers

Subscribe to Philip Chalmers: eMailAlertsEmail Alerts
Get Philip Chalmers: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Article

Robust CF Session Management Part 1 of 2

Robust CF Session Management Part 1 of 2

Browsers and what users can do with them create a minefield for Web app developers. Users can:

  • Disable all cookies. This creates unpleasant problems for CF's session management.
  • Do something else for half an hour and then try to resume where they left off. Now the session data has probably timed out, but your app gets a page request that assumes the session data is still available.
  • Trample all over the dialog logic by using Back, Forward, and Refresh, or by cloning browser windows. This may create a major security hazard.
  • Enter the site at an inner page via a search engine result, a bookmark, or a URL e-mailed to them.
  • Hit the Submit button several times, especially if the Web or your server is running slow. They may think it's a harmless way of letting off steam.
  • Use proxy servers that serve as much as possible from their own caches so different users get the same versions of pages - sometimes including the same CF session-management cookies - so the users are sharing a session!
  • Open your app in different browsers (e.g., IE and Netscape) so that CF creates separate sessions.

    What can you do to make your app less vulnerable to the slings and arrows of outrageous users? And how should you keep the user informed of what's going on? (If you don't, he or she is likely to make mistakes and eventually avoid your site if the consequences of the mistakes are serious.)

    My suggestions don't depend on the use of JavaScript; about 12% of users disable JavaScript or use non-JavaScript browsers (see www.upsdell.com/BrowserNews/res_design.htm).

    Fusebox designers can easily adapt these ideas, for example, by substituting "fuseaction" for many occurrences of "page".

    This article ignores the additional issues raised by clustered servers that make it virtually impossible to use session or updatable application variables. If your site uses or may use clustered servers, Marc Funaro's June 2000 article, "So You Want to Manage a Session on Load-Balanced Servers?" (CFDJ, Vol. 2, issue 6), explores the design and conversion issues thoroughly.

    I'm assuming here that session-level data will be stored in session variables. I don't assume that client variables will be stored in any particular way.

    I also don't cover how to combat skilled and well-equipped hackers who, for example, can detect and read cookies passed over a communications channel. If you're that concerned about security, you probably need to run your entire site through SSL and then add measures to combat the weaknesses in SSL.

    This article is in two parts. Part 1 summarizes my recommendations and discusses why you need to use cookies for session management, as well as the detection and handling of timeouts. Highlights of Part 2, which will appear in a forthcoming issue of CFDJ, are listed at the end of this article.

    Summary in Advance
    In case you know most of this already, or want to use this article as a reference, here's a summary:

  • Use cookies to identify your session. Passing CFID and CFTOKEN as URL parameters creates a security hazard for your app and for the user.
  • Make all cookies per-session, including CFID and CFTOKEN. This will make it easier to persuade users to accept your cookies, and further reduces privacy and security risks to both sides.
  • If you use CFLOCATION, always code ADDTOKEN="No" to avoid showing CFID and CFTOKEN as URL parameters.
  • Each app should use its own session management cookie containing a list of name-value pairs delimited by nondisplay characters.
  • Each page except the point of entry to your app must check whether cookies are enabled, and each page, including the point of entry, must set a "cookies enabled" name-value pair in the session management cookie for use in this check.
  • If cookies are disabled, explain to the user why per-session cookies are the most secure way to manage a conversation.
  • The "please enable cookies" page should always link to the start page rather than the page the user requested.
  • Check for session timeouts. If a timeout occurs, tell the user what has been lost and what has not - for example, "Order number 12456 has been recorded in the database".
  • To do this, your session management cookie needs a name-value pair for every logical group of data items. Set the name-value pair when the logical group is populated and delete it if the logical group is cleared.
  • You should generally save a "last transaction was ..." name-value pair in the cookie so that your "session has timed out" message can help the user to avoid reentering transactions that were completed.
  • When you display a "session has timed out" message, delete the relevant items from your session management cookie to prevent the dialog from looping.
  • If you store updated application-level data, application timeouts are an issue. In this case you probably need to save the updated application data periodically in a database and run a scheduled task to purge out-of-date entries from the database.
  • CFLOCK reads and writes of session data and of updatable application data.
  • To avoid trouble with Back, Forward, multiple Submits, and cloned browser windows, maintain a session-level page counter and include the counter value as a HIDDEN INPUT in every form. Then check that the returned value matches the expected value every time your app is asked to update the database or important session data.
  • Avoid requesting updates via links, as you'd then have to include the page counter value as a URL parameter and the user could edit it.
  • To avoid trouble with proxy servers, add a random number as a URL parameter in all links and FORM ACTIONs.
  • If you need to prevent or control the use of multiple sessions with the same user, require users to log in and maintain an application-level structure keyed on userid and containing the date and time of the last page request. Schedule a process to delete entries older than the session timeout interval.
  • You can't force the user to log out - he or she can close the browser window.

    Cookies
    Web Apps Need Cookies

    CF relies on getting CFID and CFTOKEN from the browser to identify each session. Without this information it treats each page as a new session. This is not a CF limitation, it's a consequence of the statelessness of the Web.

    There are two possible ways of passing CFID and CFTOKEN between the browser and CF: (1) use cookies, or (2) pass CFID and CFTOKEN as URL parameters in every page request. The second method creates a security hazard because malicious or simply curious users may edit the URL parameters. Such hacking has cost some companies real money and might also cost users real money.

    So you need the user to accept cookies. But some users don't like cookies for reasons of security and privacy. Other users (including me) accept per-session cookies but reject persistent cookies. Your chances of persuading the user to accept cookies will improve a lot if you promise to use only per-session cookies, that is, cookies that aren't saved to disk and are scrapped when the browser is closed.

    Using only per-session cookies also improves the security of your app because they aren't stored when the browser is closed and hence the user can't edit them in order to hack a later session.

    CF automatically creates cookies for CFID and CFTOKEN unless your CFAPPLICATION tag says SETCLIENTCOOKIES="NO". By default CF makes the CFID and CFTOKEN cookies persistent, that is, they're saved to disk. This code will make them per-session cookies:

    <cfif IsDefined("Cookie.CFID") AND
    IsDefined("Cookie.CFTOKEN")>
    <cfset cfid_local = Cookie.CFID>
    <cfset cftoken_local = Cookie.CFTOKEN>
    <cfcookie name="CFID" value="#cfid_local#">
    <cfcookie name="CFTOKEN" value="#cftoken_local#">
    </cfif>

    onRequestEnd.cfm looks like an obvious place to do this, but onRequestEnd.cfm isn't processed if your app issues a CFABORT or CFEXIT, for example, in an error handler, or if there's no Application.cfm in the same directory as onRequestEnd.cfm.

    So I prefer to make the CFID and CFTOKEN cookies per-session as early as possible, preferably in a CF template called by all output pages - for example, in a template that creates the app's logo or navigation bar.

    Check That Per-Session Cookies Are Enabled
    You can't read a cookie back in the page request that creates it because the cookie hasn't been sent to the browser yet. It will always appear as though the browser has accepted the cookie because CFCOOKIE automatically creates the corresponding cookie variable.

    In general, all pages (action as well as display) must check that the expected cookies exist, except for your startup page, which can't check for cookies the first time it's opened. If your startup page is reentered later - if it's a search page, for example - you could make it check for cookies. But this is tricky because you'd need a session variable to count entries to the start page, and the session variable could time out.

    All display pages must create a test cookie in order to test for cookies. The cookie must have the same name throughout the app. Don't use Cookie.CFID and Cookie.CFTOKEN for this check, because CF will automatically create these cookie variables before they're sent to the browser unless your CFAPPLICATION tag says SETCLIENTCOOKIES="NO".

    All other pages (action as well as display) must check for the test cookie's "cookies enabled" value before creating the test cookie. If the check returns "undefined", they should display a "please enable cookies" page that links to your startup page. But these pages have no way of knowing whether the check returned "undefined" because cookies are disabled or because the user has entered the site by the back door (e.g., via a bookmark on an inner page) and hence the test cookie hasn't yet been sent to the browser. So the wording of your "please enable cookies" page must also allow for the possibility of entry by the back door.

    The "please enable cookies" page should always link to the start page rather than the page the user requested. It would be user-friendly but dangerous to make the "please enable per-session cookies" page link to the page the user requested (including any URL parameters because:

    1. The requested page may create session-level data that won't be available to later pages if the user didn't actually enable per-session cookies before clicking the link, because CFID and CFTOKEN are needed in order for CF to find session data. In addition, the requested page may need previously created session-level data that won't be available because the "please enable per-session cookies" page generally won't send the correct values of CFID and CFTOKEN even after the user enables cookies. CF keeps creating new values of CFID and CFTOKEN since it's not getting them from cookies sent by the browser.

    2. In the worst case, if the user has been playing with the browser's cookie settings, CF may receive old values of CFID and CFTOKEN and your app will either find that the corresponding old set of session variables has timed out or (much more dangerous) will use the old session values.

    3. The user may have tried to enter the site by the back door, bypassing your security and dialog management.

    Beware of CFLOCATION
    CFLOCATION has two dangerous features: (1) it discards all previously built output, including cookies (in other words, it starts a new session!); and (2) its default is to set AddToken="Yes", so CFID and CFTOKEN are appended to the URL!

    I wish I could say, "Don't use CFLOCATION," but the safest way to display a custom error page is to CFLOCATION to a static HTML page.

    So always code ADDTOKEN="NO".

    Checking for Timeouts
    Timeouts on application and session variables are:

    • Necessary to prevent servers' memory from being swamped.
    • A degree of security for the user. They reduce the risk that someone else will use your application while the user's away from his or her desk.
    • A dangerous nuisance when they happen, both for the user and for the developer. In the worst case you could perform a database update outside a valid context.
    You Can't Avoid Session-Level Timeouts by Using Cookies
    It would be nice to avoid session-level timeouts by storing session-level data in cookie variables. But browsers place tight limits on the number and size of cookie files (see Netscape's specification for cookies at http://home.netscape.com/newsref/std/cookie_spec.html):
    • A maximum of 300 cookies in total; if a browser hits this limit, it discards the least recently used cookie
    • No more than 20 cookies per site
    • Maximum size is 4KB per cookie (including headers, delimiters, etc.)
    CF makes the size limitation a bit more stringent. It encodes cookie data with a URL encoding scheme that expands alphanumeric data by about 30% and nonalphanumeric data by a factor of 3. It also throws an error if you try to store more than 4,000 encoded bytes of data in a cookie. And it automatically uses separate cookies for CFID, CFTOKEN, and (if your CFAPPLICATION has CLIENTSTORAGE="COOKIE") CFGLOBALS (this contains client variables URLToken, Hit-Count, TimeCreated, and LastVisit). That's two or three of your twenty used already.

    So most apps won't be able to store all their session-level data in cookie variables. Some session-level data shouldn't be stored in cookies in any case, as you'll see later.

    Timeout-Handling Requirements Vary
    We have to deal with a variety of situations. In some cases you need to check for timeouts only when the user requests a page that needs some session data. For example, in a shopping cart the customer's login is often required only for the checkout process and for updating the customer's registration details.

    In other cases you may need to check constantly to see whether session data has timed out. For example, in a shopping cart you should warn the user immediately if the cart's contents have timed out, even if the page the user requested doesn't need the cart. Otherwise, when the user asks the app to display the cart or go to the checkout, he or she will find items are missing from the cart and will lose confidence in your app. Even worse, the user may not notice and so place an incomplete order, discovering the omissions only when it's delivered.

    In general you need a separate check on the status of each logical group of session variables. Logical group is a rather fuzzy term, but I suggest logical groupings are largely based on the intended life cycle of the information. For example, login data will ideally persist until the user closes the browser (or logs out or in as someone else, depending on the system), but shopping cart data should be cleared immediately after an order is placed.

    Logical groupings must also take account of whether you need to check the data on every page request or only when the user requests a page that needs the data.

    Using Cookies to Detect and Handle Session Timeouts
    A timeout is often detected when you get a request for a page that requires the data and it no longer exists. But if you're checking some session variables on every page request, you need to distinguish between a variable or a data structure that has timed out and one that just hasn't been populated yet (or has been cleared, e.g., a shopping cart when an order has been processed).

    For each logical group of session data you need a "session data created" indicator that isn't invalidated by a session timeout. You should set this indicator when information is added to the logical group, rather than when an empty logical group is created (by CFPARAM, for example). And you should clear or delete the indicator if the logical group is emptied - for example, when a shopping cart is cleared at the end of the checkout process.

    Here are the options I'm aware of for storing these indicators:

    1. In a per-session cookie that's unique to your app, a cookie with a maximum size of 4,000 bytes (including headers and delimiters), should be enough to store a few hundred "session data created" indicators.

    2. You could maintain an application-level structure keyed on Session.SessionID and containing a "session data created" indicator for each logical group (probably as an array). This approach has the major weakness that application data can also time out. If application timeout intervals are long, you also need to write and schedule housekeeping processes to remove entries for expired sessions.

    3. You can use a database in the same way. This costs additional disk accesses and processing time, but avoids the application timeout problem and conserves memory in heavily loaded servers, especially if application timeout intervals are long.

    You'd still need to write and schedule housekeeping processes, though, and the database must be one that is good at reusing space freed by deleted records (MS Access is not).

    For most apps per-session cookies are the simplest and most robust approach. To minimize the risk of too many cookies or of a session management cookie being overwritten, you should use one session management cookie per app, with a name that's unique to the app. It will then be read and rewritten by every page request, so the risk of its being discarded is very small.

    You should store the app's session management information in this cookie as name-value pairs delimited by a character you won't use in a name or a value. I prefer delimiters that have no display values or effects - for example, from CHR(1) to CHR(6) CF automatically URL-encodes them when sending and URL-decodes them on receipt. This cookie should also contain your "cookies enabled" name-value pair.

    Your session management cookie will be a list of lists, and CF's list-handling functions make it easy to manage this type of structure. In particular, you can easily unpack it into a session-scoped structure in order to add, change, and remove name-value pairs, and then reassemble the cookie before sending the next page.

    I recommend a session-scoped structure because it has one set of values per session and you can CFLOCK updates to it, increasing your app's protection against simultaneous updates from cloned windows.

    When you display a "session has timed out" message, delete the relevant item from your session management cookie to prevent the dialog from looping. This is particularly important if you check some logical data groups on every page request. For example, if you check the shopping cart's status on every page request and don't clear the "cart expected" indicator for the cookie, each page request will show the session timeout and the user will never get a chance to put anything in the cart.

    If you have an enormous app with too many "session data created" indicators to fit in a cookie, you may need to use a database table as described above.

    Which Pages Check for Which Session Variables?
    Obviously, any page that needs a session variable to have a real value (not just a default) should check it. Otherwise it depends on the app. I've already suggested that every page in a shopping cart app should check that the cart's contents have not been lost by a timeout.

    What Should the Timeout Warning Page Say?
    If the user can enter several transactions in a session, the simple message:

    Sorry, your session has timed out. All the information you entered has been lost could mislead the user into reentering transactions that have already been successfully completed. It would also be misleading if some session variables have timed out but others are okay. For example, if the user:

    • Logs in to place an order
    • Doesn't use the app for longer than the timeout interval
    • Places more items in the shopping cart for a second order
    • Goes to the checkout
    the login data will have timed out, but the shopping cart's contents will be okay.

    In this type of situation you need to tell the user explicitly what has been lost and what is okay, and reassure him or her that the last completed transaction (if any) has been recorded on the database, preferably with a link to a confirmation page.

    To achieve this your session management cookie should keep a note of completed transactions by including, for example, a name-value pair "LastOrder : 123456". You can then use this to display the last order number in the timeout warning page and as a URL parameter in the link to the confirmation page.

    Checking for Application-Level Timeouts
    There's no problem if your application-level data is constant. It will always be refreshed from Application.cfm or equivalent. But if it's updated to accumulate statistics, for example, it's not enough just to detect a timeout, you need to forestall it. One way to do this is to run a scheduled task at intervals of 5 minutes less than the application timeout to record the accumulated data in a database and reset the application-level counters. The scheduled task must also reset the application timeout clock so it would have to use the same application name..

    In Part 2 of this article we'll look at:

    • Back, Forward, Refresh, cloned windows, and multiple Submits
    • Proxy servers
    • Users entering your site via an inner page
    • Multiple sessions with different browsers
    • Closing a session
    • References and acknowledgments
  • More Stories By Philip Chalmers

    Philip Chalmers has been working in information technology since the early 1970s. He’s relatively new to ColdFusion, but has specified, designed, and developed systems on platforms ranging from mainframes to PCs in about a dozen languages. His first technical publication appeared in 1979, when he wrote several sections of the Adabas Design Guide.

    Comments (0)

    Share your thoughts on this story.

    Add your comment
    You must be signed in to add a comment. Sign-in | Register

    In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.