Mitigating Denial of Service Attacks with Web Resources

We found a potential Denial of Service (DOS) attack using Web resources, and fixed it...
Another interesting security discussion from our on-going security push and the saga around finding a solution that's worth sharing (and hopefully reading)... this time it's about Web Resources which I had blogged about here.

As background for this, recall that the URL for a Web resource takes the form: WebResource.axd?a=MyControls&r=Bold.gif&t=632059604175183419

The interesting parameter is "t". This parameter is the timestamp of the assembly containing the resource. We add this parameter to the URL, so that rebuilding the assembly generates a new URL, causing the browser to ignore what it has cached, and fetch the new data. We aggressively cache for best performance by telling the browser to cache the data and never check for an updated version. Then we vary the output cache by the "t" parameter on the server.

Unfortunately however, this means that some malicious person out there could be simulating requests with random "t" values, and we'd just fill up the cache. Therein lay the denial of service (DOS) attack. Though these are somewhat hard to deal with, we didn't want to contribute to the problem.

So the first thing that we could do was validate that the incoming "t" value matched the timestamp of the assembly or returning an error status code otherwise. This takes care of the DOS attack, but introduces a new problem. The timestamp of the assembly on each machine on a Web farm isn't the same. So this simply doesn't work.

The next solution was to drop the "t" and put in assembly version. This actually got checked in... but it actually breaks the development scenario (oops!), where the assembly version isn't updated every build (nor should it be). In fact, assembly versions typically don't change even when a patch is put out. For example, the assembly version of System.Web doesn't change between QFEs. So this change obviously had to be backed out, but before making anymore changes, we had to ensure we had a solution that worked.

To lay down the requirements: We needed a "t"-like parameter so the browser's cache model would work. This also enabled the control development scenario really well, wherein changes were automatically picked up when the resource was modified. Pretty compelling and not something to give up on just yet. But we also didn't want to vary the cache per "t", since that led to the security hole.

A little discussion actually led us to realize a solution that was actually pretty easy, and was probably staring at us for the longest time. Just needed a few folks from the team to gather around and have a focused discussion on the solution. By default we were storing the cached data in disk-output cache, whose lifetime went beyond the lifetime of an app-domain. The "t" was allowing us to work against the disk-based output cache. What we simply needed to do was turn off disk-based output cache, and rely on in-memory cache alone (we still get the full benefits of the cache, including kernel-mode caching, so we aren't losing much). Now, whenever the bin directory was updated causing a restart or the application was explicitly restarted to pick up GAC changes, the in-memory cache would be cleaned up, and new content would be read from the assembly resource on-demand. To finish off the fix, we just needed to drop the version parameter, and bring back "t" with its prior semantics sans the vary-by behavior.

That takes care of denial of service attacks and retains performance. Just what we were looking for!

As a side note, watch out what you're caching in your own application as well, and make sure you're not susceptible to attacks that make you flood the cache with spurious entries.
Posted on Tuesday, 12/14/2004 @ 2:02 AM | #ASP.NET


Comments

4 comments have been posted.

Sergio Pereira

Posted on 12/14/2004 @ 6:17 AM
This post provided me great insight on caching from the DoS attacks perspective. To be honest I had never give this scenario a great deal of thought. What do you think should be some of our steps when using parameterized output cache? Maybe validate that each of the parameters has a valid value ?

Victor Garcia Aprea

Posted on 12/14/2004 @ 2:20 PM
Hi Nikhil,

When I last looked at AssemblyResourceLoader's code (like 1 year ago when writing this[1]) I noticed another possible attack where a malicious user can create fake WebResource URLs at the client side using well-known names for assemblies stored in the GAC (Crystal*, Microsoft.VisualStudio.*, ect). This, of course, won't cause any resources to be served (as there will be no WebResourceAttribute allowing this), but it will cause the specified assembly to be loaded into memory by the WebResource handler. So a couple of faked URLs could cause your Web application to load lots of unnecessary assemblies, thus considerably increasing its memory working set.

I'm curious on how you approached this.

[1] http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvs05/html/webresource.asp

Nikhil Kothari

Posted on 12/25/2004 @ 5:28 PM
Sergio...
I'll try to collect some more definitive guidelines on cache usage from the team here, but I would imagine the basic thought is to validate input, and vary entries by only what you expect to have change. Another thing to think about: you almost never want to create a cache entry per user; unless your app has very few users, its not going to scale.
Another thing that comes to mind is in some cases your app might be more suited to cache data as opposed to the response and save trips to the database instead. Here you can have full control over what goes into cache, what the cache key is etc. The Whidbey data source controls definitely make this approach easier, and coupled with SQL cache invalidation, its extremely powerful.

Vic...
Excellent point, and something we should look into.
I'll need to go back and check if using the new Whidbey reflection-only assembly loading would be useful here. As I understand it code in those assemblies does not run, and they can be unloaded (I think). One thing to keep in mind though - this would be useful only for assemblies in the GAC. Assemblies that are in the bin directory already get loaded whenever we need to search for types in various other spots.
Another thought is to only load assemblies that have been listed in the <compilation> section in configuration. This would definitely fix the issue, so that random assemblies are not loaded at all. Any thoughts on this approach?

Maestrocity

Posted on 12/2/2005 @ 9:53 PM
Hack of the first order. Use what static files use, etags. I thought embedding resources would be way cool, make installation and maintanence of controls easier etc. but when I found out that every new session will require most browsers to re-request the entire resource rather than 304 I figured no way. Not worth the hit in bandwidth.
The discussion on this post has been closed. Please use my contact form to provide comments.