I keep running into code lately that does some pretty elaborate security testing but in the end leaves a very large issue unaddressed — canonicalization errors related to file I/O.
Michael Howard and David LeBlanc dedicated an entire chapter in Writing Secure Codeto the idea that “All Input is Evil“. The concept is that you should not rely on the fact that input coming from any source — trusted or not is going to be in a format that you accept. You should validate, validate, and validate again even if your I/O function isn’t intended to access anything of great importance.
Canonicalization is the process of breaking something down to it’s most simple form. Let’s take a look at an example:
public FileStream OpenFile( string fileName )
{
string fullPath = string.Format(@”C:inetpubwwwroot{0}”, fileName);
return new FileStream(fullPath, FileMode, FileAccess);
}
You may have ony intended to give this function access to code stored in the default web root. But what would happen if someone called your function like this:
File f = OpenFile(”..\..\windows\system32\config\sam”);
The elipsis at the begining of the path would direct the file to go up a directory and doing it twice would put the user at your root directory, giving someone access to your entire file system.
You could use a regular expression to check for this type of thing right? Well, yes, but it wouldn’t be enough. Your regular expression would test for the “..\” sequence, but what happens if someone encodes the path with %2E instead of the “.”. Would your regular expression be smart enough to check for that? Probably not. There are a ton of ways to represent a path differently and you should use many different forms of validation. However, one of my favorite is to use the DirectoryInfo and FileInfo “FullName” property. Creating a new FileInfo instance passing it your intended path will break the path down into it’s actual meaning. Once this is done, you can use the FullName member to obtain the canonicalized path. As a matter of completion, here is some sample code to get you started:
public FileStream OpenFile( string fileName )
{
string fullPath = string.Format(@”C:inetpubwwwroot{0}”, fileName);
string canonicalizedPath = new FileInfo(fullPath).FullName;
return new FileStream(canonicalizedPath, FileMode, FileAccess);
}
Another option available is to use the Path class as follows:
public FileStream OpenFile( string fileName )
{
string fullPath = string.Format(@”C:inetpubwwwroot{0}”, fileName);
string canonicalizedPath = Path.GetFullPath(fullPath);
return new FileStream(canonicalizedPath, FileMode, FileAccess);
}
This performs the same work under the covers without creating a FileInfo instance.
This example does not explain everything that a dilligent coder worried about security should do to secure this method, but it should demonstrate the usefulness of the FullName member in validating file paths.