Navigation

>> Home
 
ASP.NET (13)
Security (5)
"Classic" ASP (2)
C# (1)
VB.NET (1)
ASP and Flash (1)
 
About Us
 
Chinese Content German Content

 

Trap Alert: Files that aren't

Written by: Christoph Wille
Translated by: Bernhard Spuida
First published: 1/31/2002

As a matter of fact, everybody ought to be familiar with the FileSystemObject - it is used for reading and writing files as well as for certain important operations (e.g. deleting) on files and directories. However, it is little known what Windows (NT/2000) actually considers to be a file - this goes well beyond files residing on disk drives.

Under Windows, not only files residing on disks are considered to be files - devices (COM or LPT ports), consoles (CON or AUX) as well as named pipes (as used by e.g., SQL Server) also are considered as files. Now the question for me as an ASP programmer is: why should I see this as interesting? Let us consider this harmless looking example (readfile.asp):

<%
   Const ForReading = 1, ForWriting = 2, ForAppending = 8
   Set fso = Server.CreateObject("Scripting.FileSystemObject")
   Set f = fso.OpenTextFile("c:\temp\COM1", ForReading, True)
   strRetVal = f.ReadLine
   f.Close
   Response.Write strRetVal
%>

The c:\temp\COM1 is no mere joke of mine, it is perfectly valid under Windows, as in this manner we may directly address devices from the console (the directory or drive may be choosen arbitrarily, to put it this way...). What will happen then? As soon as I connect to this device, the thread the ASP page runs in hangs until a timeout is returned by from the port. As ASP is served from a thread pool, I can thus elegantly run a DoS (Denial of Service) attack against a server - if it is vulnerable.

And vulnerable we are quickly - certainly you will be able to name a number of Web sites that pass filenames via the QueryString to be read and used in the content returned.

Hint This thing with the file in the QueryString is dangerously common, I saw it happen quite a number of times. Now there are further points of attack coming into play: sites that during upload allow the user to choose the filename, programs that generate files (and their names) from user input and so on in this vein. This danger is not fictitious, take a close look at your code!

Should you now counter by saying that you are checking for the file's existence, consider the following script (fileexists.asp):

<%
   filespec = "c:\temp\COM1"
   Set fso = Server.CreateObject("Scripting.FileSystemObject")
   If (fso.FileExists(filespec)) Then
      msg = filespec & " exists."
   Else
      msg = filespec & " doesn't exist."
   End If
   Response.Write msg
%>

Alas - as COM1 is a valid file - FileExists returns True. And then we perform a bona fide file access and our Web server is stuck!

Reserved Words for Devices

After this initial warm up to our subject matter at hand, I want to serve up a list of reserved words for devices that may be used to achieve this effect:

  • COM1-COM9
  • LPT1-LPT9
  • CON, PRN, AUX, CLOCK$, NUL

In any arbitrary directory, these reserved words are invalid for "real" filenames, however, they can be adressed through the Windows File APIs. And to top things off, we can do the following, too:

Response.Write fso.FileExists("c:\temp\NUL.txt") ' returns True

With the file extension being completely arbitrary...

How to Prevent the Crash?

There is a number of ways to check the passed or used filenames for validity:

  • Should the filename contain one of the reserved words listed above between a / and a dot (or end of string), the 'file' won't be opened. This check can be performed by normal string operations or using regular expressions.
  • We check the type of the file before touching it.

The latter version has one significant advantage - it protects us from attacks against named pipes as well as against reserved words added in the future (should this happen). The basic check of filenames should be performed in any case. The closer we scrutinize, the less will escape us.

Checking the File Type

That FileExists of the FileSystemObject won't help us we already learned. And unfortunately, the FileSystemObject does not offer a function for retrieving the file type. Therfore I decided to write a small component in C++ using ATL (sourcecode included in the download accompanying the article) which determines the file type.

Before listing the code of the component SecurityEnhance.FileUtilities, I think that most readers will be interested to see how the component may be used. Therefore I wrote a nifty little test script that tests a number of diverse file types (filetypecheck.asp):

<%
Option Explicit
Const RET_FILE_TYPE_UNKNOWN        = 1
Const RET_FILE_TYPE_DISK        = 2
Const RET_FILE_TYPE_CHAR        = 3
Const RET_FILE_TYPE_PIPE        = 4

Sub FileCheck(ByVal strFile)
  Dim objSecurityCheck, nFileType, nErrorCode, strError
  Set objSecurityCheck = Server.CreateObject("SecurityEnhance.FileUtilities")

  On Error Resume Next
  nFileType = objSecurityCheck.GetFileType(strFile)
  nErrorCode = Err.Number
  strError = Err.Description
  On Error GoTo 0  ' re-enabling error handling clear the Err object

  If (0 = nErrorCode) Then
    Response.Write strFile & ": is of type " & nFileType
  Else
    Response.Write strError
  End If
  Response.Write "<br>" & vbCrLf
End Sub

FileCheck "c:\temp\thisfiledoesnotexist.txt"
' above will return error 2, "The system cannot find the file specified. "
FileCheck "c:\temp\COM1"
' above will return type 3 (character file) typically an LPT device or a console.
FileCheck "c:\boot.ini"
' above returns error 5, "Access is denied." (hopefully)
FileCheck "c:\view.txt"
' above returns type 2, disk file (if file exists of course)
FileCheck "c:\COM1.txt"
' also returns type 3 - attention!
FileCheck "c:\COM1somemoretext"
' this does not work (2: file not found)
FileCheck "c:\somefile.COM1"
' as well as this (2: file not found)
%>

Hint Even though I documented the more important Win32 error numbers in the source code, it might be possible for others to occur. To go from error number to descriptive text, just enter net helpmsg nnnn at the command prompt, where nnnn is the error number.

The output - even though the comments in the script say the same beforehand - looks like this:

Basically the GetFileType method is governed by this rule: all besides type 2 (RET_FILE_TYPE_DISK) won't be touched even with a ten foot pole - for security considerations, such 'files' should be considered to be 'off limits'. Keeping this in mind, no one can con us with a 'file' that will crash the server.

Before we now get down to the code of the component and how it retrieves the file type - the component is of itself intended for read only access of files (the file must exist). This is not bad in itself, as types other than 2 (RET_FILE_TYPE_DISK) behave the same for reading and writing: is the type returned without error and unequal 2, then the file won't be written.

To make things clear once more: no matter whether we want to read a file or write to it, whenever the type returned by GetFileType is not equal to 2, the operation will not be performed for security reasons.

The Component

As showdown for the terminally interested (everybody else may safely branch to the conclusion), now the relevant code of the component. Even though C++ may be unfamiliar to many, I can assure you that it isn't hard at all (from FileUtilities.cpp):

#define E_FILEERROR    MAKE_HRESULT(1,FACILITY_ITF,1)
#define RET_FILE_TYPE_UNKNOWN 1
#define RET_FILE_TYPE_DISK 2
#define RET_FILE_TYPE_CHAR 3
#define RET_FILE_TYPE_PIPE 4

STDMETHODIMP CFileUtilities::GetFileType(BSTR FileName, long *FileType)
{
  USES_CONVERSION;
  *FileType = -1;

  // open the file for generic reading
  HANDLE hFile = ::CreateFile(W2A(FileName), 
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE, 
            NULL, 
            OPEN_EXISTING, 
            0, 
            NULL );

  if (INVALID_HANDLE_VALUE == hFile)
  {
    TCHAR achErrFormatting[256];
    wsprintf(achErrFormatting,
          "The file %s could not be accessed. The Win32 error code is %ld.",
          W2A(FileName), ::GetLastError());
    Error(achErrFormatting, 0, NULL, IID_IFileUtilities, E_FILEERROR);
    return E_FILEERROR;
  }
  else
  {
    DWORD dwFileType = ::GetFileType(hFile);
    switch (dwFileType)
    {
    case FILE_TYPE_UNKNOWN: *FileType = RET_FILE_TYPE_UNKNOWN; break;
    case FILE_TYPE_DISK: *FileType = RET_FILE_TYPE_DISK; break;
    case FILE_TYPE_CHAR: *FileType = RET_FILE_TYPE_CHAR; break;
    case FILE_TYPE_PIPE: *FileType = RET_FILE_TYPE_PIPE; break;
    default: *FileType = -1;
    }
    ::CloseHandle(hFile);
  }
  return S_OK;
}

I highlighted the two important functions CreateFile and GetFileType. They do the actual work, the rest is merely frosting to correctly handle the errors and clean up everything shipshape. It would have been nice, had Microsoft thought of such a function for the FileSystemObject.

Conclusion

Today's article once again demonstrates impressively that all input we accept for use in our applications can be very dangerous. In any case, the simple check should be used in your scripts, however, it is better and safer to use the component.

Downloading the Code

Click here to start the download.

©2000-2004 AspHeute.com
All rights reserved. The content of these pages is copyrighted.
All content and images on this site are copyright. Reuse (even parts) needs our written consent.