This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


MIND


This article assumes you're familiar with WNet API and Visual Basic
Download the code (64KB)

Security with Windows NT and IIS: A Primer
By Jeff Niblack

Is setting up the proper level of security on your IIS installation proving to be a headache? This guide to your options can ease the pain.
Chances are, it hasn't taken you long to encounter little security gremlins running amok in your Web applications built on Microsoft® Internet Information Server (IIS). Often, a few minutes spent refreshing your memory related to Windows NT® security and IIS fundamentals is all that's needed to resolve the issue. Occasionally, however, you'll fall headfirst into that deep, dark crevasse of impersonation. No matter how hard IIS tries, your Web apps fail and your users' screams grow louder.
      For Web developers who must support line-of-business apps where files are scattered throughout the maze of corporate-networkdom, the problem only escalates. I know developers who have spent hour after hour trying to get IIS to allow access to secured network drives only to find out that it can't be done without compromising the security of the targeted resource—or worse yet—the entire Web site. With a little ingenuity, along with the help of the Windows® networking (WNet) API and Visual Basic®, there is hope.

One Step Backward
      Before I venture any further, let's take a step back to look at the fundamentals of Windows NT and IIS security. The most important rule about Windows NT security is that all processes running on a Windows NT-based machine require and are run using a valid Windows NT account. There are no exceptions. Simply put, when a process or program (such as IIS) performs a task on behalf of a user, the task is performed with the user's security limitations. The whole point is to allow the process or program to perform the task with no more and no less access to resources or files than the user would have if they had logged on locally. This process is called Impersonation.
      IIS, on the other hand, is designed to handle HTTP requests regardless of who the user is or even if information about the user is not known. To this end, IIS handles two basic types of requests, anonymous and authenticated. The IIS Authentication Methods properties box, shown in Figure 1, lists the three supported methods. Allow Anonymous Access does just what it says—anyone and everyone can access the site. Basic Authentication and Windows NT Challenge/Response are both authenticated methods.

Figure 1: IIS Authentication Methods
      Figure 1: IIS Authentication Methods

      Anonymous requests are the most common requests handled by HTTP servers. In this mode the server knows nothing about the user short of an IP address, browser type, and a few other nuggets of general information. Since all tasks performed under Windows NT require a valid Windows NT account, IIS needs an account to use when handling these requests. Therefore, when IIS is installed, by default the installation process creates a local user account, known as the IUSR account, with IUSR_machinename as the user name. This account is then added to the Guests group on the machine. When IIS receives an anonymous HTTP request, it impersonates the IUSR account to perform the task. Because the IUSR account by default has rights similar to that of the Guest account, access is very limited. In most cases (such as simple HTTP PUTs of HTML files), this isn't a problem. However, many of today's Web applications require access to secured resources such as database servers or network drives.
      Authenticated requests require a valid Windows NT account. IIS receives the required user information in one of two ways: Basic Authentication and Windows NT Challenge/Response (otherwise known as NTLM). With Basic Authentication the user is prompted for a user name and password, which is then Base64-encoded and passed to IIS. Once IIS receives the user's information and verifies it against the Windows NT user database on the Web server or an available domain controller, IIS will impersonate the specified user. This is great for Web developers because the required user information is provided. Unfortunately, it's also great for hackers since Base64- encoded information is very easily broken.
      NTLM is similar to Basic Authentication in that user information is provided to IIS. However, NTLM doesn't actually pass the user name and password over the wire. Instead, a one-way encryption method is employed to authenticate the user with a hash value. This is clearly the most secure form of authentication that IIS supports. Unfortunately for Web developers, the user name and password aren't available to IIS. This effectively prevents IIS from impersonating a user when needed, and as a result requests to secured resources will fail.

Uh-Oh, the Crevasse
      So what's a Web developer to do? Well, the options are actually pretty straightforward. If no sensitive or secured resources are used or needed, Anonymous Access is the easiest to implement, and is probably sufficient. Otherwise, Windows NT Challenge/Response is the way to go.
      What about using Basic Authentication? Because Basic Authentication passes the user name and password over the wire using an easily and commonly broken encryption scheme, this method is very dangerous. Even the most novice of hackers can break Base64-encoded information in minutes.
      What about the fact that Netscape Navigator only understands Basic Authentication? Microsoft has provided a plug-in for Netscape Navigator that allows Navigator to support Windows NT Challenge/Response authentication.
      OK, so you've set up your Web site to only use Windows NT Challenge/Response authentication and now your Web app needs access to a secured network drive, say the drive that holds year-to-date sales and contacts information. No matter how hard you try, IIS will never be able to access the network drive. Why not? Although, Windows NT Challenge/Response will make sure each user has a valid Windows NT account, it doesn't have the user name and password. Therefore, when IIS requests access to the network drive by impersonating the user, the request fails. That is, of course, unless you can create an ActiveX® component that allows IIS to dynamically connect to the network drive.

Here Comes the Sun
      By tapping the power of the WNet API and using a few Visual Basic statements, you can provide IIS with the necessary tools to access secured network resources and still use Windows NT Challenge/Response authentication. Using this approach, you give access to a Windows NT account that the Web application uses, rather than to each user of the Web application. This effectively lowers the administration overhead required for the Web app and significantly reduces the total cost of the application. In addition, the complexity of many corporate Web applications is reduced simply because the security model is simplified. Rather than ensuring that each user has the appropriate access, the access question lies within the application itself. Essentially, if the user has access to the Web application (ensured by using Windows NT Challenge/Response), they have access to the resources the Web application can access.
      To implement the solution, begin by creating (adding) a connection using the WNetAddConnection2 function, and performing a task. Then delete (cancel) the connection using the WNetCancelConnection2 function. Along the way, WNetGetLastError returns any errors that occur when using the WNet functions.
      Now let's jump right into creating a simple ActiveX component and ASP page. My task is to get the contents of an ASCII text file (ytd_ sales_report.txt) located in the root directory of a secured network drive. The Universal Naming Convention (UNC) name for the network drive is \\acct01\ sales. The Windows NT account I'll use to access the UNC is acme\mgr1 as the qualified user name, with OgreaT1 as the password. Finally, I'll use W: as the local drive identifier.
      If you want to actually test this out, change the UNC, Windows NT account information, and drive indicator appropriately. If you don't have a network to test with, you can also read files on the local drive. To read the autoexec.bat file on the C: drive, for example, use C: as the drive, set the UNC and Windows NT account information to empty strings, and set the file name to autoexec.bat.
      First, let's create a simple ActiveX component with Visual Basic that your IIS machine can use. Start Visual Basic 6.0 and select New Project. Select ActiveX DLL and click the OK button. Visual Basic creates Project1 with Class1 as a class module.
      In the General/Declarations section of Class1, enter the code shown in Figure 2, which defines the constants and WNet Datatypes and Functions. Now copy the basic get file function shown in Figure 3 into Class1. In the properties box for Class1, verify that Instancing is set to 5 (see Figure 4 ). That is, MultiUse and (Name) is GetFileTest.
      Next, open the Project Properties box by selecting the Project | Project1 properties menu item. Change the default General options to match the ones shown in Figure 5. Before going any further, save the project and compile the ActiveX DLL.

Figure 5: Project Properties Box
      Figure 5: Project Properties Box

      Now that you have an ActiveX DLL to perform your task, test it in Visual Basic before moving on to the ASP page for the Web server. With MINDSample as the active project in Visual Basic, select the File | Add Project menu item and add a Standard EXE project. Visual Basic will create Project1 with Form1 as a form. Add a textbox to Form1 named Text1 that is the same size as the form. You'll use it to hold the contents of the ASCII files you get with the ActiveX DLL. Change the default properties of Text1 as follows:

 MultiLine = True
 ScrollBars = 3 - Both
      Now add a code module by selecting the Project | Add Module menu item and clicking the Open button. Copy the following code into the newly created Module1:

 Sub Main()

     Dim objTemp As Object
     Dim strTemp As String

     Set objTemp = CreateObject("MINDSample.GetFileTest")
     
     strTemp = objTemp.sGetFile("W:", "acme\mgr1", _
         "OgreaT1", "\\ACCT01\Sales", "\", _
         "YTD_SALES_REPORT.txt")
     
     'show file contents
     Form1.Text1.Text = strTemp
     Form1.Show vbModal
     
     'clean up
     Set objTemp = Nothing
     
     End
     
 End Sub
      In the Project Explorer box, right-click Project1, as shown in Figure 6 and select Set as Startup. This ensures that your test project (Project1) will be started when you run your Visual Basic program. Finally, open the properties box for Project1 by right-clicking Project1 in the Project Explorer and selecting Project1 Properties. Select Sub Main as the Startup Object and click the OK button.
      After saving the entire project group and compiling Project1, press F5 to run the test program. The contents of the target text file should display in Text1 of Form1.

A là ASP
      Now that you've tested the ActiveX DLL and it works, let's create an ASP page that will do the same thing as Project1 did in your Visual Basic project group. First, save the code in Figure 7 as GetFile.asp on your Web server. If you're using Personal Web Server on the same machine you used to compile the ActiveX DLL, it was automatically registered during the compilation. However, if your Web server is on a different machine, you'll need to register it. To do so, copy the ActiveX DLL to the Web server. From a command prompt, set to the directory that the DLL was copied into, enter: regsvr32 MINDSample.dll. You should receive a "DLLRegisterServer in MINDSample.dll succeeded" message. Next, type in the URL for the GetFile.asp page and click the Read File button. This time, the contents of the text file will be shown in the browser. Congratulations, you've just done something IIS isn't supposed to be able to do!

Dive into the Code
      So now that you've built a simple control and proven that IIS can do the impossible, let's break down the code. So strap on your API toolbelt 'cause I'm taking you down into the code!
      Since the solution centers around the ActiveX DLL, let's take a look at it first. In my sample DLL, I used two WNet functions (WNetAddConnection2 and WNetCancelConnection2) and a user-defined type (NETRESOURCE). These functions and type were used to add/create a network connection and terminate/cancel the connection after completing the task.
      The NETRESOURCE structure is required when adding a network connection using the WNetAddConnection2 function. Although there are eight components that make up the NETRESOURCE structure, I'll only use three: dwType, lpLocalName, and lpRemoteName. Here's the entire NETRESOURCE type definition:


 Private Type NETRESOURCE
     dwScope As Long
     dwType As Long
     dwDisplayType As Long
     dwUsage As Long
     lpLocalName As String
     lpRemoteName As String
     lpComment As String
     lpProvider As String
 End Type
      dwScope specifies the scope of the network resource that is being defined. In my solution this value is actually ignored, so I won't define a value for it.
      The dwType argument is used to indicate the type of resource that is being defined. Since I'm dealing with network drives, I'll set this to RESOURCETYPE_DISK or &H1.
      dwDisplayType is used to indicate how the connection should be displayed in a user interface. Since this doesn't apply here, I won't do anything with it. Likewise, dwUsage is only used when dwScope is set to RESOURCE_GLOBALNET or &H2, so there's no need to set this either.
      lpLocalName and lpRemoteName, however, are key to the solution. lpLocalName is used to specify the local device name or drive identifier. You'll need this device name when you actually do something with the connection, so you'll need to set it. In my example, I used W:, but you can use any available drive identifier. lpRemoteName is used to define the remote network name. This would normally be a UNC value.
      The last two components of the NETRESOURCE type are lpComment and lpProvider. Neither of these are required in this example, so I'll just leave them undefined.
      WNetAddConnection2 is the function used to add or create the network connection. The WNet API actually defines three different functions to add connections: WNetAddConnection, WNetAddConnection2, and WNetAddConnection3. WNetAddConnection is included only to provide compatibility with previous versions of Windows. WNetAddConnection3 is functionally equivalent to WNetAddConnection2, with one exception: WNetAddConnection3 includes an additional parameter (hwndOwner) that is used to provide a handle to a window for any network provider dialog boxes. This means that WNetAddConnection2 provides the functionality you need. Here's the Visual Basic function declaration:

 Private Declare Function WNetAddConnection2 Lib "mpr.dll"
 Alias "WNetAddConnection2A" _
     (lpNetResource As NETRESOURCE, _
     ByVal lpPassword As String, _
     ByVal lpUserName As String, _
     ByVal dwFlags As Long) As Long
      The lpNetResource parameter should be the NETRESOURCE defined previously. lpPassword and lpUserName are used to specify the Windows NT account information, if necessary. dwFlags is used to specify various connection options. In this case, I want the connection to be remembered (persistent) so I'll set this value to CONNECT_UPDATE_ PROFILE or &H1.
      If successful, a call to the WNetAddConnection2 function will return NO_ERROR or 0. Otherwise, a failure occurred. In the sample ActiveX DLL, I've limited the scope of the error routine to only identify that an error occurred, not specifics about the error. (I'll talk more about this later.)
      Here's an example of connecting to \\TEST1\TESTAREA using TestUser as the Windows NT account (the password is "test") and W: as the local drive.

 Dim udtNetResource As NETRESOURCE
 Dim lngRC As Long
 
 'set up the NETRESOURCE structure
 udtNetResource.dwType = RESOURCETYPE_DISK
 udtNetResource.lpLocalName = "W:"
 udtNetResource.lpRemoteName = "\\TEST1\TESTAREA"
 
 'add the connection
 lngRC = WNetAddConnection2(udtNetResource, "test", _
     "TestUser", CONNECT_UPDATE_PROFILE)
 
 'check return code
 If lngRC <> 0 Then
     MsgBox "Error!"
 Else
     MsgBox "Successful!"
 End If
      To cancel a connection, the WNetCancelConnection2 function is used. Once again, for backward compatibility reasons, the WNet API includes the WNetCancelConnection function for older versions of Windows. The Visual Basic declaration for the WNetCancelConnection2 function is:

 Private Declare Function WNetCancelConnection2 Lib "mpr.dll"
 Alias "WNetCancelConnection2A" _
     (ByVal lpName As String, _
     ByVal dwFlags As Long, _
     ByVal fForce As Long) As Long
The lpName parameter contains the name of the local device that was added using a previous call to the WNetAddConnection2 function. dwFlags is used to specify connection options. In this case, since I added a persistent connection, I want to also update the profile to indicate that the connection was cancelled or terminated. Setting dwFlags to CONNECT_UPDATE_PROFILE or &H1 accomplishes just that. The fForce parameter is used to indicate whether a connection should terminate even if there are open files or jobs on the connection. This value is set to either &H1 (TRUE) or &H0 (FALSE).
      If successful, a call to the WNetCancelConnection2 function will return NO_ERROR or 0. Otherwise, a failure occurred. Once again, I'm not concerned with handling the errors at this point, so I'll just let the user know than an error occurred. With all that in mind, here's an example of canceling the network connection I previously added:

 lngRC = WNetCancelConnection2("W:", CONNECT_UPDATE_PROFILE, &H1)
 
 'check return code
 If lngRC <> 0 Then
     MsgBox "Error!"
 Else
     MsgBox "Successful!"
 End If
      Now that you know the purpose of the WNet API functions and type and how to call them, let's use them to do something after you have a connection. In the sample ActiveX DLL, you simply wanted to read the file that was specified. Visual Basic can easily provide this functionality by opening the file for Input and then reading and saving each line until you hit the end of the file.

 intFile = FreeFile()
 
 'put together the target path
 strTarget = strDrive & strDirectory & strFile
 
 Open strTarget For Input As #intFile
 
 strOutLine = ""
 
 Do While Not EOF(intFile)
     Line Input #intFile, strInLine
     strOutLine = strOutLine & strInLine & vbNewLine
 Loop
 
 Close #intFile
Aside from a few basic statements to trap the Visual Basic errors and return the contents of the text file, that's about it for the sample ActiveX DLL.
      The code for the Visual Basic test program is very straightforward and involves only a few lines:

 Dim objTemp As Object
 Dim strTemp As String
 
 Set objTemp = CreateObject("MINDSample.GetFileTest")
 
 strTemp = objTemp.sGetFile("W:", "acme\mgr1", _
     "OgreaT1", "\\ACCT01\Sales", "\", _
     "YTD_SALES_REPORT.txt")
 
 'show file contents
 Form1.Text1.Text = strTemp
 Form1.Show vbModal
 
 'clean up
 Set objTemp = Nothing
      My sample defines two local variables, objTemp and strTemp. Use objTemp as the variable to hold the pointer to the GetFileTest function in the ActiveX DLL. Once you've initialized the objTemp variable using the Set statement, you can make the call to the sGetFile method. Use the return value of sGetFile to hold the contents of the file you read, which is then shown using the Form you created. Finally, release all the system and memory resources associated with objTemp.
      The sample ASP code is nearly the same as the test Visual Basic program. The only differences involve displaying the contents that were returned from the call to the sGetFile method of the ActiveX DLL. Instead of showing the contents in a form, wrap the contents up with the <pre> block element so the displayed text uses the formatting included in the text file.

 <%If strMsg <> "" Then%>
 Return Message:<br>
 <pre><%=strMsg%></pre><br>
 <hr>
 <br>
 <%End If%>

Whoops!
      That wraps it up for the sample. Be aware that there are numerous holes in the code and lots of room for improvement. To plug some of those holes I've included a fully developed ActiveX DLL, included in the Dir_Classes Visual Basic project, at the link at the top of this article, which tightens up the error handling so that detailed error messages are returned to the user. In the General/Declarations area of the ActiveX DLL, I define the NO_ERROR constant:


 Private Const NO_ERROR = 0
This is used for each call to a WNet API function to verify if an error occurred. If the return code from the function call doesn't match NO_ERROR, then the ErrorUpd procedure is called with the LastDllError and Description from the Visual Basic Err object.

 ErrorUpd Err.LastDllError, Err.Description
      The ErrorUpd procedure is used to update two module-level variables, mlngErrorNum and mstrErrorDesc. These variables are accessed by calls to two properties:

 Public Property Get lErrorNum() As Variant
     lErrorNum = mlngErrorNum
 End Property
 Public Property Get sErrorDesc() As Variant
     sErrorDesc = mstrErrorDesc
 End Property
A third property, sError, is included to return a string with mlngErrorNum and mstrErrorDesc concatenated.

 Public Property Get sError() As Variant
     sError = mlngErrorNum & ": " & mstrErrorDesc
 End Property
      Once inside the ErrorUpd procedure, the error number is assigned to the mlngErrorNum variable and is compared against the error numbers that are returned by WNetAddConnection2 or WNetCancelConnection2. If a match occurs, the mstrErrorDesc variable is updated with a detailed error message. When a network provider-specific error occurs (indicated by an error number of ERROR_EXTENDED_ ERROR or 1208), the WNetGetLastError WNet API function is called. Here's the Visual Basic declaration for the WNetGetLastError function.

 Private Declare Function WNetGetLastError Lib "mpr.dll" _
     (lpError As Long, _
     lpErrorBuf As String, _
     nErrorBufSize As Long, _
     lpNameBuf As String, _
     nNameBufSize As Long) As Long
      Each of the five parameters defined above are used to return information from the network provider. lpError is the actual error number that occurred. lpErrorBuf is the actual error message, and nErrorBufSize is the length of the error message in lpErrorBuf. Finally, lpNameBuf is the name of the network provider with nNameBufSize holding the length of the network provider name in lpNameBuf.
       Figure 8 shows some sample Visual Basic code that gets the network provider-specific errors.

Odds and Ends
      In addition to the error handling, Dir_Classes includes various methods to perform tasks such as copying, deleting, and moving files and making, removing, and listing directories. While this takes care of some of the more common tasks, you could quickly add some on your own. For example, how about a directory compare or file compare method? Whatever the task, I've covered the basics for allowing IIS to connect to and use secured network drives. The next step is up to you.

MSDN
http://msdn.microsoft.com/library/devprods/vs6/vstudio/
vsentpro/veconconfiguringsecurityforinternetinformationserver.htm

and
http://support.microsoft.com/support/kb/articles/q229/6/94.asp


From the October 1999 issue of Microsoft Internet Developer.