Thursday, 16 December 2010

[.NET] Determining File Mime Types (Correctly!)

If you have used an ASP.NET upload control, you may have been reliant on getting the ContentType property and making security decisions based on the value from that. However, this isn't truly accurate.
What .NET does here is take the uploaded file and look at the file extension. It will then feed the extension into a mime library and generate the relevant content type.

I.e.
Upload a .doc file - ContentType will read: "application/msword"

For this to fail, we could rename an executable file (.exe) to a .doc and upload it. The ContentType will still be application/msword as it's based off the extension.

To overcome this problem, there are two methods we can use. Both of them involve peeking into the file and fetching some data relevant to it's type. So there is a natural trade-off with performance vs. security.

The first example below uses InterOp, and gives us an accurate mime type for the file. The second example uses feature codes, the only down side to using this is where you wish to accept doc and ppt but not xls. This isn't possible as these all share the same feature code (see below).



Two Examples to make decisions based on file/mime type
[System.Runtime.InteropServices.DllImport("urlmon.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode, ExactSpelling = true, SetLastError = false)]
static extern int FindMimeFromData(IntPtr pBC,
[System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] string pwzUrl,
[System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPArray, ArraySubType = System.Runtime.InteropServices.UnmanagedType.I1, SizeParamIndex = 3)] 
byte[] pBuffer, int cbSize,
[System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] string pwzMimeProposed, int dwMimeFlags, out IntPtr ppwzMimeOut, int dwReserved);
 
/// <summary>
/// Ensures that file exists and retrieves the mime type
/// </summary>
/// <param name="fileName">Full path to the file</param>
/// <returns>Returns the Mime Type</returns>
public string GetMimeFromFile(string fileName)
{
    IntPtr mimeout;
    if (!System.IO.File.Exists(fileName))
    throw new System.IO.FileNotFoundException(fileName + " not found");
 
    int MaxContent = (int)new System.IO.FileInfo(fileName).Length;
    if (MaxContent > 4096) MaxContent = 4096;
    System.IO.FileStream fs = System.IO.File.OpenRead(fileName);
 
    byte[] buf = new byte[MaxContent];
    fs.Read(buf, 0, MaxContent);
    fs.Close();
    int result = FindMimeFromData(IntPtr.Zero, fileName, buf, MaxContent, null, 0, out mimeout, 0);
 
    if (result != 0)
    throw System.Runtime.InteropServices.Marshal.GetExceptionForHR(result);
 
    string mime = System.Runtime.InteropServices.Marshal.PtrToStringUni(mimeout);
    System.Runtime.InteropServices.Marshal.FreeCoTaskMem(mimeout);
 
    return mime;
}
 




/// <summary>
/// Reads the file to determine the feature code.
///     /* Extension name specification [Feature codes]
///     *7173        gif  
///     *255216      jpg 
///     *13780       png 
///     *6677        bmp 
///     *239187      txt,aspx,asp,sql 
///     *208207      xls,doc,ppt 
///     *6063        xml 
///     *6033        htm,html 
///     *4742        js 
///     *8075        xlsx,docx,pptx,mmap,zip,msi
///     *8297        rar
///     *01          accdb,mdb 
///     *7790        exe,dll
///     *5666        psd  
///     *255254      rdp  
///     *10056       bt  
///     *64101       bat
///     *3780        pdf
///     */
/// </summary>
/// <param name="hifile">Posted file following the upload process</param>
/// <returns>Returns weather the file type is allowed</returns>
public bool IsAllowedExtension(HttpPostedFile hifile)
{
    bool ret = false;
 
    System.IO.BinaryReader r = new System.IO.BinaryReader(hifile.InputStream);
    string fileclass = string.Empty;
    byte buffer;
 
    try
    {
    buffer = r.ReadByte();
    fileclass = buffer.ToString();
    buffer = r.ReadByte();
    fileclass += buffer.ToString();
    }
    catch
    {
    return false;
    }
    r.Close();
 
    String[] fileType = {"208207", "8075", "3780"}; // Place allowed file types here
 
    for (int i = 0; i < fileType.Length; i++)
    {
    if (fileclass == fileType[i])
    {
        ret = true;
        break;
    }
    }
 
    return ret;
}

No comments: