ASP.NET MVC Tip #40 - Don’t Cache Pages that Require Authorization

In this tip, Stephen Walther warns you to avoid making a mistake that can result in private data being displayed to unauthorized users.

Imagine that you are building a financial services website. Each user of the website can log in and view a page that lists their current investments. For example, the investments page might show a report displaying how much you have invested in different stocks, bonds, and mutual funds.

Now, imagine that your website becomes wildly successful. It is used by billions of users every day. Your website performance starts to degrade. Each time a user visits their personalized investments page, a complicated query must be performed against the database which makes the investments page slow. What do you do?

Here are some options:

Option #1 – Go hide in a cave somewhere.

Option #2 – Enable page output caching for the investments page.

Option #3 – Buy more server hardware.

Close your eyes and think about your answer for a moment. No cheating! No reading ahead! I’m patient. I’ll wait right here.

Okay, this was a trick question. If you picked Option #2 then you have just done something horribly wrong. Shame on you! Let me explain.

You can enable output caching for any page within an ASP.NET Web Forms website by adding the following directive to the top of the page:

<%@ OutputCache Duration="60" VaryByParam="none" %>

This directive caches the page for 60 seconds so that the content of the page does not need to be generated again for each user.

Within an ASP.NET MVC application, you can use the OutputCache attribute on a controller action like this:

[OutputCache(Duration = 60)]
public ActionResult Index()
{
  var investments = from i in _dataContext.Investments select i;
  return View("Index", investments);
}

This OutputCache attribute caches the controller response to the browser request for 60 seconds.

Using the OutputCache directive or OutputCache attribute to cache the results of a request dramatically improves the performance of your website. When you enable caching, your database server does not need to regenerate the investments report page with each browser request.

Unfortunately, however, when you cache a page, the page is cached for all visitors to the website. Caching the investments page can enable one user to see another user’s private financial data.

Imagine that Joe is the first person to view the investments page. The page displays Joe’s private financial data. You’ve enabled output caching so the page is cached in memory.

Now, imagine that Mike requests the same investments page with the 60 second time interval. In that case, Mike will see Joe’s private financial data. Mike will see the private data rendered from the server cache.

Therefore, never (ever, ever) cache a page when the page contains data that is private to a particular user. If you cache the page, then the private data will be displayed to anyone who visits the website.

In general, you should only enable caching for a page when the page does not require authorization. Normally, you require authorization for a page when you display personalized data in the page. Since you don’t want personalized data to be shared among multiple users, don’t cache pages that require authorization.

Within an ASP.NET MVC application, never add both the [Authorize] and [OutputCache] attribute to the same controller action:

[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()
{
  var investments = from i in _dataContext.Investments select i;
  return View("Index", investments);
}

In this code, you are combining caching with per-user authorization. Most likely, if you are adding both the Authorize and OutputCache attribute to the same controller action then you are broadcasting private user data to the entire world. This code is a very fragrant security smell. Don’t do it!

12 Comments

  • I think you're conflating two separate issues here. First, the OutputCache attribute issue appears to be ab ug in our implementation that we're looking at.

    Blanketly saying you shouldn't cache authenticated content in ASP.NET is not exactly correct. In ASP.NET MVC Preview 5, I would agree with you, but that's because of a bug we just identified, not because it should blanketly never happen.

    But in regular ASP.NET there's many scenarios for caching authenticated content. Perhaps I'm caching the same content for all authenticated users. Or maybe I'm using "donut caching". With normal ASP.NET, this all works because it runs the authentication phase before the caching phase.

    With MVC, for example, you can still use URL Authorization (if you're careful) with OutputCaching and it all works together properly.

    The issue around user specific content is completely a separate issue. You can cache user content if you vary the cache by user. Maybe you have a small user base with an expensive calculation to render the page.

    Also keep in mind, this isn't even an authentication issue. Some sites show info particular to a user without authentication such as the sites that ask you if you want to meet other singles near your city. Caching that wouldn't make sense unless you varied the cache by user or by location. No authentication is involved in that scenario.

  • You've been haacked (a bad thing?)!!

  • Haacked seems ill...maybe better not to post on a Friday night? What does "Blanketly" mean?

  • You could use VaryByCustom and override GetVaryByCustomString to enable safe output cache for users

  • Anders - true, but if you start output caching per-user content, your cache is likely to fill so quickly that it becomes useless. The scenario for output caching privileged content really needs coarser granularity; e.g. per role.

  • Keep in mind that caching on the client (cache-control: private) is not the same as caching on the server (OutputCache) or caching for third-party public servers (cache-control:public). Often you want to tell *clients* to cache items privately for any authenticated content, but you do not want public servers to cache this content.

    As for whether you use ASP.NET/MVC to create a server-side cached copy of the data, that's a completely different story.

  • @Mike -- good point! Not caching on the server and only caching on the client (with cache-control:private) would be a good way to improve performance without introducing the danger of leaking private data across users. And, using the OutputCache NoStore property would make the situation even safer.

  • Of course you can cache an authenticated page, just use a better cache key - e.g. a CustomCacheKey which you then fill in with the authenticated user's ID. VaryByParam="None" is obv the wrong way to derive a cache key.

  • Good post Stephen! However in my opinion ASP.NET Cache's problems go deeper than simply having security lapses. Now don't get me wrong for single server environments it can do wonders, but as you start going into larger environments, it cannot scale.

    For more information on all the various bottlenecks please visit my blog or check out: www.alachisoft.com/ncache/asp-net-cache.html

  • I cannot agree with you.
    In the MVC Beta, it is ok to use [Authorize] and [OutputCache] attribute on the same action.
    below is the code from mvc framwork

    AuthorizationContext authContext = InvokeAuthorizationFilters(methodInfo, filterInfo.AuthorizationFilters);
    if (authContext.Cancel) {
    // not authorized, so don't execute the action method or its filters
    InvokeActionResult(authContext.Result ?? EmptyResult.Instance);
    }
    else {
    ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(methodInfo, parameters, filterInfo.ActionFilters);
    InvokeActionResultWithFilters(postActionContext.Result, filterInfo.ResultFilters);
    }

    And i have tested for your situation, and it work well.

  • Leegool,

    you are kind late on that comment, Phil said it was a bug and they were fixing it.

  • Asp net mvc tip 40 don t cache pages that require authorization.. Keen :)

Comments have been disabled for this content.