Learning While Aging

Batch update your Web.config to workaround ASP.NET security vulnerability

[UPDATE]: There is no need for using this tool to update your web applications, because Microsoft has released the official ASP.NET security fix through Windows Update: http://weblogs.asp.net/scottgu/archive/2010/09/30/asp-net-security-fix-now-on-windows-update.aspx

You may have already known the newly discovered ASP.NET security vulnerability, and the suggested workaround is to modify your Web.config file until Microsoft releases a security path, as mentioned in Scott’s blog: http://weblogs.asp.net/scottgu/archive/2010/09/18/important-asp-net-security-vulnerability.aspx. I hope you have already updated your application according to the workaround. However, what if you have tons of applications running? Do you want to update one by one? I certainly don’t want to go this route, and that is why I wrote a small console application that will update all my applications’ Web.config files for me. Actually, my department used my application today and updated about 200 web applications on both our staging server and production server, it saved use a lot of time.

Here is what the console application does:

1. it gets the physical paths of the applications that you specify in the App.config file of the console application

2. It loops through each path to see if there is a Web.config file. Once it finds one, it parses the Web.config file until it finds <customErrors> node.

3. Then it will check what language the application is written with, and copy an built-in error page (check Scott’s blog for how to write the error page) to the root folder of your application, and rename it to “AppError_GUID.aspx”, where GUID stands for a Guid string. Since this new error page is using inline model, you don’t need to redeploy your application.

4. Make a copy of your current Web.config file by renaming it to WebConfig.cs (you can change this by modifying the source code)

5. Change <customErrors> node to this:

<customErrors mode="On" defaultRedirect="AppError_26545ce8-80d1-4acd-bb07-6a2ce2385dcd.aspx">
 </customErrors>

6. Save the Web.config

7. Generate a log file for your record

Here is the App.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="WebAppRootPath" value="C:\Inetpub\wwwroot"/>
    <add key="ApplicationsToUpdate" value="App1|App2|App3"/>
  </appSettings>
</configuration>

The first key in the config file is the root of your applications, the second key is the list of applications you would like to update with this console application. For example, if your applications are running under D:\Inetpub\wwwroot, and you want to update MyApp1, MyApp2, MyApp3, and MyApp4 (the physical path will be D:\Inetpub\wwwroot\MyApp1, D:\Inetput\wwwroot\MyApp2, etc.), then you need to change the config file as follows:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="WebAppRootPath" value="C:\Inetpub\wwwroot"/>
    <add key="ApplicationsToUpdate" value="MyApp1|MyApp2|MyApp3|MyApp4"/>
  </appSettings>
</configuration>

Then this console application will only update MyApp1, MyApp2, MyApp3, and MyApp4, and leave all other applications alone.

Here is the full source code of the console application:

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Xml;
using System.Reflection;
using System.Configuration;
using System.Management;

namespace WebConfigWorkaround
{
    class Program
    {
        static void Main(string[] args)
        {
            StringBuilder sbPath = new StringBuilder();
            StringBuilder sbLog = new StringBuilder();
            sbLog.Append("Update started at: ");
            sbLog.Append(DateTime.Now.ToString());
            sbLog.Append(Environment.NewLine);

            string rootDirectory = ConfigurationManager.AppSettings["WebAppRootPath"];

            string[] appsUpdateList = ConfigurationManager.AppSettings["ApplicationsToUpdate"].Split('|');
            for (int i = 0; i < appsUpdateList.Length; i++)
            {
                string appPath = rootDirectory + @"\" + appsUpdateList[i];
                DirectoryInfo di = new DirectoryInfo(appPath);
                // Let us loop through all directories under the root directory
                // and search for Web.config file to update

                if (di.Exists)
                {
                    FileInfo fiWebconfig;
                    FileInfo[] files = di.GetFiles("Web.config"); // root level Web.config
                    if (files.Length == 1)
                    {
                        fiWebconfig = files[0];
                        sbLog.Append(fiWebconfig.FullName);
                        if (UpdateWebConfig(fiWebconfig.FullName))
                        {
                            sbLog.Append("      Successful");
                        }
                        else
                        {
                            sbLog.Append("      Failed");
                        }
                        sbLog.Append(Environment.NewLine);
                    }
                    // Then search the first-level subdirectories for Web.config
                    // If you need to search more levels, then create a
                    // recursive function to do it.
                    foreach (DirectoryInfo subdir in di.GetDirectories())
                    {
                        FileInfo[] configFiles = subdir.GetFiles("Web.config");
                        if (configFiles.Length == 1)
                        {
                            fiWebconfig = configFiles[0];
                            if (UpdateWebConfig(fiWebconfig.FullName))
                            {
                                sbLog.Append("      Successful");
                            }
                            else
                            {
                                sbLog.Append("      Failed");
                            }
                            sbLog.Append(Environment.NewLine);
                        }
                    }
                    // end of finding Web.config
                }

            } // end of loop
            sbLog.Append("Update finished at: ");
            sbLog.Append(DateTime.Now.ToString());
            sbLog.Append(Environment.NewLine);
            sbLog.Append(Environment.NewLine);

            string logFilePath = @"C:\log.txt";
            GenerateLogFile(logFilePath, sbLog.ToString());

        } // end of main program

        #region Update Web.config file

        public static bool UpdateWebConfig(string filePath)
        {
            bool result = true;
            XmlDocument xDoc = new XmlDocument();
            try
            {
                xDoc.Load(filePath.ToString());
                XmlElement xError = xDoc.SelectSingleNode("//system.web/customErrors") as XmlElement;
                if (xError != null)
                {
                    string ErrorFileName = "";
                    if (xError.Attributes["defaultRedirect"] != null)
                    {
                        string oldErrorFileName = xError.Attributes["defaultRedirect"].Value;

                        if (oldErrorFileName.IndexOf("AppError_") != -1)
                        {
                            ErrorFileName = oldErrorFileName;
                        }
                        else
                        {
                            Guid fileGuid = Guid.NewGuid();
                            ErrorFileName = @"AppError_" + fileGuid.ToString() + @".aspx";
                        }

                    }
                    else
                    {
                        Guid fileGuid = Guid.NewGuid();
                        ErrorFileName = @"AppError_" + fileGuid.ToString() + @".aspx";
                    }

                    // Clear <error> node
                    xError.RemoveAll();

                    xError.SetAttribute("mode", "On");
                    xError.SetAttribute("defaultRedirect", ErrorFileName);

                    // Now, let's copy AppError.aspx to the application
                    // Because the built-in AppError.aspx is written with inline model
                    // it does not matter what language your application is written with

                    DirectoryInfo di = new DirectoryInfo(filePath);
                    DirectoryInfo diParent = di.Parent;

                    string consoleAppFolder = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);

                    FileInfo fiError = new FileInfo(consoleAppFolder + @"\AppErrorCSharp.aspx");
                    if (fiError.Exists)
                    {
                        fiError.CopyTo(diParent.FullName + @"\" + ErrorFileName, true);
                        // Let's make a copy of the old web.config file before modification
                        // I change the extension to .cs to protect the backup
                        // but if the backup already exists, then we can skip the backup
                        FileInfo fiWebConfigBackup = new FileInfo(diParent.FullName + @"\WebConfig.cs");

                        if (!fiWebConfigBackup.Exists)
                        {
                            File.Copy(filePath, diParent.FullName + @"\WebConfig.cs", false);
                        }
                        xDoc.Save(filePath);
                    }

                } // end of checking <customErrors> node
            }
            catch (Exception ex)
            {
                result = false;
            }
            finally
            {
                xDoc = null;
            } // end of try
            return result;
        } // end of UpdateWebConfig method

        #endregion

        #region GenerateLog
        public static void GenerateLogFile(string logFilePath, string logString)
        {
            FileStream fs = File.Open(logFilePath, FileMode.OpenOrCreate, FileAccess.Write);
            StreamWriter sw = new StreamWriter(fs);
            try
            {
                sw.Write(logString);

            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {

                sw.Close();
                fs.Close();
            }
        }

        #endregion

    }// end of class
}

If you would like to download a copy of the solution file of the console application, then click HERE, and you need Visual Studio 2008 to open it.

Important Notes

I wrote this application  in a short amount of time, so it is not perfect. You may find places need to be optimized, and you are welcome to download the source code to modify it. Also keep it in mind, I provide this tool free of change, and you use this application at your own risk, so I suggest you to go through the source code to understand it before running it, and remember to make a copy of all your web application just in case something goes wrong. My comments in the source code above has detailed explanation for almost each step.

Known Issues:

1. You have to run this console application with the administrative privilege, otherwise, it will throw access denied error on you.

2. The console application will NOT find nested Web.config file. You will need to modify the source code and accomplish it.

3. If you accidently ran this application more than one, it would NOT generate duplication AppError_GUID.aspx file, and would NOT overwrite your backup file of Web.config.

4. If your application is using .NET framework 3.5 SP1 and above, you will need to manually modify <customError> node, after running this console, by adding this attribute to <customErrors>:

redirectMode="ResponseRewrite"

as suggested in Scott’s blog.

How to roll back to the original state before running this console application?

According to Scott, this workaround is temporary and will not be needed once a security patch is released by Microsoft. So how can you roll back to the original state after the security patch is released?

I will write another console application (hopefully soon) to roll back the changes this console have made in a batch mode. However, you can always roll back manually:

1. Delete AppError_GUID.aspx

2. Delete Web.config file

3. Rename WebConfig.cs back to Web.config

That’s it. If you have questions about this console application, please let me know.