Altering User-Agent Changes Output in ASP.NET Bundling and Minification

This post covers an ASP.NET feature where user-agent manipulation changes the output in your production environment. How much can be revealed on your servers?

I almost fell out of my chair today while reading the article ASP.NET MVC bundles internals, specifically this one sentence:

If you would like to check which files were included into a bundle you may tamper the request by modifying the User-Agent header to Eureka/1.

What does this mean?

It means that by changing the user-agent, you can get the unminified, comments-included source code of your bundles... in your production environment!

This article shows how to reproduce, and subsequently how to stop, this little-known feature.

Is this a huge deal?

Not really... I mean it's just comments, right? And developers NEVER put things into comments that they wouldn't want their clients to see.
According to ASP.NET:

Minification performs a variety of different code optimizations to scripts or css, such as removing unnecessary white space and comments.

They left out "unless you know the secret User-Agent that, when used, renders the previous statement... FALSE."

Yeah. I feel betrayed. I didn't ask for a secret knock to allow DEBUGGING on my PRODUCTION SERVER that I NEVER KNEW ABOUT. What else do they have hidden...? I'm waiting to find out that changing the User-Agent to DonkeyKnockers/314 will allow for tracing on my server. You know... JUST IN CASE.

Back to the point. Here is how to reproduce it, and also how to stop it.

See it in Action

For this test I setup a new project in VS2012 and installed the newest version of Microsoft ASP.NET Web Optimization Framework (1.1.3, released 2/20/2014). I enabled optimizations and gave it a whirl. Standard bundle.cs file:

public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/modernizr").Include("~/Scripts/modernizr-*"));

    bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));

    BundleTable.EnableOptimizations = true;
}

Viewing my bundled and minified style sheet looked just like I expected it to look:

An image showing how bundles should look.

So now let's use Eureka/1. In Chrome, hit F12 to bring up DevTools. Now click the "Show Console" icon next to the gear in the upper right. Click the "Emulation" tab -> User Agent -> Spoof user agent and set the value to "Eureka/1.0".

How to change your user-agent in Chrome

Now refresh the page... and you get this.

Oops. Now your bundle isn't so minified, is it?

Now What

I don't want that to happen... luckily you can write a class that inherits from IBundleBuilder to override this feature. Or, you can get my NuGet package to address this and another issue, as specified in Preserving Important Comments in Bundling and Minification. Also, vote to have the hack removed on CodePlex.

Before I made that NuGet package, my solution was this:

In my bundle.cs file, I added two new classes (yes, yes, both classes have a Read method... I did that for ease of use to just copy a class for testing... ideally you'd centralize Read):

public class ScriptBundleBuilder : IBundleBuilder
{
    public virtual string BuildBundleContent(Bundle bundle, BundleContext context, IEnumerable files)
    {
        var content = new StringBuilder();
        foreach (var file in files)
        {
            FileInfo f = new FileInfo(HttpContext.Current.Server.MapPath(file.VirtualFile.VirtualPath));
            Microsoft.Ajax.Utilities.CodeSettings settings = new Microsoft.Ajax.Utilities.CodeSettings();
            settings.RemoveUnneededCode = true;
            settings.StripDebugStatements = true;
            settings.PreserveImportantComments = false;
            settings.TermSemicolons = true;
            var minifier = new Microsoft.Ajax.Utilities.Minifier();
            content.Append(minifier.MinifyJavaScript(Read(f), settings));
        }

        return content.ToString();
    }

    private string Read(FileInfo file)
    {
        using (var r = file.OpenText())
        {
            return r.ReadToEnd();
        }
    }
}

public class StyleBundleBuilder : IBundleBuilder
{
    public virtual string BuildBundleContent(Bundle bundle, BundleContext context, IEnumerable files)
    {
        var content = new StringBuilder();
        foreach (var file in files)
        {
            FileInfo f = new FileInfo(HttpContext.Current.Server.MapPath(file.VirtualFile.VirtualPath));
            Microsoft.Ajax.Utilities.CssSettings settings = new Microsoft.Ajax.Utilities.CssSettings();
            settings.CommentMode = Microsoft.Ajax.Utilities.CssComment.None;
            var minifier = new Microsoft.Ajax.Utilities.Minifier();
            content.Append(minifier.MinifyStyleSheet(Read(f), settings));
        }

        return content.ToString();
    }

    private string Read(FileInfo file)
    {
        using (var r = file.OpenText())
        {
            return r.ReadToEnd();
        }
    }
}

And then just tell your bundles to use those new builders:

public static void RegisterBundles(BundleCollection bundles)
{
    var scriptBundle = new ScriptBundle("~/bundles/modernizr");
    scriptBundle.Builder = new ScriptBundleBuilder(); //This uses that new class
    scriptBundle.Include("~/Scripts/modernizr-*");
    bundles.Add(scriptBundle);

    var styleBundle = new StyleBundle("~/Content/css");
    styleBundle.Builder = new StyleBundleBuilder(); //This uses that new class
    styleBundle.Include("~/Content/site.css");
    bundles.Add(styleBundle);

    BundleTable.EnableOptimizations = true; //so we can test locally
}

That's it! In my tests, I could no longer get the bundle to be served up unminified and with comments.

Stack Overflow user? Check out my answer on this issue, which was actually the precursor to this post.

Happy Coding!