From the November 2001 issue of MSDN Magazine.

MSDN Magazine

ASP .NET Security Issues
Keith Brown
Download the code for this article: Security0111.exe (115KB)
T

his month I'm starting a series of columns dedicated to security in the Microsoft® .NET Framework, and I figured that the best place to start would be one of its most popular features, ASP .NET.

Security versus Ease of Access

      There are lots of different types of Web sites with varying security needs. Some Web sites (search engines, for example) collect no information about their users, and publish data that is widely available. These sites don't have much to lose by having a rather open security policy, and they try to make their sites as easy to use as possible. Other sites (online banking sites, for example) may collect demographics, credit card numbers, and other personal information from their customers in order to provide their services. These Web sites need much stronger security policies. They have sensitive data, and thus their users must jump through a few more hoops to avoid opening security holes. Forcing users to view portions of a site via Secure Sockets Layer (SSL), for instance, puts a much greater load on the Web server, and this ultimately increases latency for each individual user. SSL sessions are very expensive to establish.
      It's important to note that just because a search engine doesn't deal in personal or proprietary information, it cannot simply ignore security. There are many individuals and organizations that pride themselves on being able to successfully attack popular Web sites or sites that they find offensive. Consider the myriad of government Web sites whose main pages have been replaced wholesale by an attacker, as well as the distributed denial of service (DDoS) attacks against many Fortune 500 companies.
      Anyone developing a public Web site today really needs to have a strategy for protecting their site from digital terrorists. In order to even begin developing this sort of strategy, you must first understand how the underlying platform works. With this in mind, I'll start by explaining how ASP .NET applications are hosted on the server, and discuss some server-side settings that you can use to protect your site. As I write this, the .NET platform is in its second beta. Things may change a bit by the time the platform is released.
      As a security-conscious developer, you should care what security context your code runs in. Contrary to what many developers would like to believe, it's always best to run your code with the least amount of privileges possible. This is called the Principal of Least Privilege, and even the most well-intentioned developer should stick to it because none of us are perfect. Always remember that a bad guy can exploit many, if not all, bugs in your application. Running with only the privileges you absolutely need allows the operating system to do its job and protect you from your own imperfect code. So one of the first things I'll ferret out is what security context your ASP .NET application runs in.

ASP .NET is an ISAPI Application

      Bring up the Internet Information Services (IIS) administration utility (Internet Services Manager) and create a new virtual directory. Right-click on this directory and choose Properties, then press the Configuration button. You'll see a dialog like the one shown in Figure 1, which controls the mapping of URLs to ISAPI applications. For instance, a URL that ends with .asp will be handled by the ASP.DLL application. If you've installed the .NET platform, you'll notice that there is now a mapping for several new suffixes, including .ASPX. IIS will pass requests for these pages to the ASPNET_ISAPI.DLL application.

Figure 1 IIS Script Map
Figure 1 IIS Script Map

      Just because the ASP .NET ISAPI application is packaged in a DLL doesn't mean it will run directly inside the core IIS server process, INETINFO.EXE. Since IIS 4.0 was released, developers have had the ability to surrogate their applications by choosing an application protection level, as shown in Figure 2.

Figure 2 Configuring a Web App
Figure 2 Configuring a Web App

A Web application configured to run at any protection level other than Low will be hosted in a COM+ surrogate process running as IWAM_MACHINE, an account with very few privileges. Compare this to running inside the Web server process, which runs as SYSTEM!
      An attacker who exploits an unchecked buffer in an in-process ISAPI application can run code of his choosing as SYSTEM, the most privileged of all logon sessions. If this happens, it's pretty much all over for your Web server (and perhaps your entire network if you don't discover the breach right away). So configuring your code to run outside of INETINFO is a great idea as far as security is concerned. Interestingly enough though, this isn't the end of the story for ASP .NET.

The ASP .NET Worker Process

      The ISAPI extension ASPNET_ISAPI.DLL doesn't really do that much. It certainly doesn't host your ASP .NET code. What it really does is forward the request to another process called ASPNET_WP.EXE via a named pipe. So it turns out that configuring your IIS virtual directory so that it runs out-of-process doesn't have any effect on where your managed code actually runs. Interestingly enough, that configuration option doesn't even affect the security context of ASPNET_ISAPI.DLL because at installation time it is added to an IIS metabase property known as InProcessIsapiApps, which causes it to always run in-process. So if you're building an ASP .NET application, don't waste your time configuring the application protection setting in IIS. It has absolutely no effect on where your managed code runs.
      Where does the ASP .NET worker process run, anyway? Well, as of this writing, it runs as SYSTEM. Yikes! Figure 3 shows the basic architecture, a diagram you may have seen before, but this one focuses on the security contexts involved. Before you get too flustered, let me tell you that you can absolutely control the security context of this worker process through the machine.config file.

Figure 3 ASP .NET Worker Process
Figure 3 ASP .NET Worker Process

      Here's how. First, find the machine.config file in the following directory:

  %WINNT%\Microsoft.NET\Framework\v.X.X.XXXX\CONFIG
  

 

Search for an element called <processModel>. This is where you can control the settings for the worker process. As of this writing, the default settings that control the process's security context are:

     userName="SYSTEM"
  
password="AutoGenerate"

 

      This is the same setting you'd get if you simply omitted these two attributes. Actually, in this case the password field isn't needed because aspnet_isapi.dll simply calls CreateProcess to launch the worker process, and since the DLL always runs in the SYSTEM logon session, the worker process runs there as well.
      You can change these settings in one of two ways. The first is to provide an explicit user name and password:

  userName="Alice"
  
password="OhMyGoshACleartextPassword!"

 

As of this writing, machine.config is world-readable by default, so the previous setting clearly isn't very desirable. The other documented setting is:

  userName="Machine"
  
password="AutoGenerate"

 

This causes aspnet_isapi.dll to call LogonUser and CreateProcessAsUser, placing the worker process into a sandboxed logon session with very few privileges. This is similar to the behavior you get with raw ISAPI or unmanaged ASP applications when configured to run out-of-process. In IIS, isolated and pooled applications run under a daemon account, IWAM_MACHINE, which has very few privileges. In ASP .NET, the account name is ASPNET, and has the same function. This setting will likely be the most desirable, but it's a bit buggy as of this writing. The ASP .NET team is currently working on stabilizing this feature.
      You might be wondering at this point whether you really need to worry about bugs like unchecked buffers, since you're writing managed code now. Assuming you've not turned off code verification, your managed code should be analyzed by the runtime before it is just-in-time (JIT) compiled into native code and executed. Well, that certainly is the theory, but in practice you need to consider that you'll likely be calling out to third-party native DLLs through COM interop or P/Invoke for some time to come. Remember that these DLLs are loaded directly into the worker process unless you do something special, like use a COM or COM+ surrogate process to host them. So you should absolutely care about the security context of that worker process. Also, you need to assume that there will be bugs (like unchecked buffers) found over time in the runtime itself. By running the worker process with the least possible privileges, you're helping to protect against an attack on the unmanaged code in your system.

IIS and Impersonation

      IIS normally runs ISAPI extensions using an impersonation token, and this token is usually for the anonymous Internet user, IUSR_MACHINE, although if other authentication options have been selected, it might represent some other principal. What this means to an unmanaged ISAPI application or a pre-.NET ASP page is that there are two security contexts available: that of the process and that of the thread. By default, virtually every Windows® API that you call uses the thread token for access checks, which means if IIS is impersonating IUSR_MACHINE, you'll only be able to do things that IUSR_MACHINE is allowed to do.
      However, impersonation in Windows was designed for use in trusted server processes. If you make a call out to a random DLL, expecting it to run with only the privileges afforded to IUSR_MACHINE, you may get a big surprise. There is nothing stopping that DLL from simply removing the thread token by calling RevertToSelf and running in the security context of the process. Once again, impersonation was designed from the beginning with the assumption that it would only be used from trusted code. This is why it's tremendously important to choose your process's security context wisely, whether you're building managed or unmanaged server applications. You need to assume that a compromised DLL could always remove the thread token in an attempt to elevate its privilege level.
      So how does this affect managed ASP .NET applications? Well, by default the token that IIS is impersonating stays inside IIS. It's not used at all in the worker process. There's only one identity that your ASP .NET application will see by default, and that's the process identity. That is, unless you configure your ASP .NET Web application to impersonate.
      In your application's web.config file, you can configure ASP .NET to perform a hand-off of the impersonation token from aspnet_isapi.dll to the worker process. If you do this, the physical thread on which your managed code runs will be impersonating, just like IIS normally does. The configuration looks like this:

  <configuration>
  
<system.web>
<identity impersonate='true'/>
</system.web>
</configuration>

 

      To see how this works, use my Token Dump component that can be downloaded from the link at the top of this article.
      Create a virtual directory for yourself that points to, say, c:\temp. Install and register tokdumpsrv.dll, run TLBIMP to get a managed wrapper for it, and pop it into the c:\temp\bin directory. Now cut and paste the following ASPX page into c:\temp:

  <%@page language='C#'%>
  
<%@import namespace='TOKDUMPSRVLib'%>
<%= ((ITokDump)new TokDump()).TokenDump(1) %>

 

      If you surf to this page with your browser (via HTTP, of course), you'll be making the entire transition through INETINFO, into the unmanaged ISAPI application aspnet_isapi.dll, over to the worker process via a named pipe, into the managed code that was compiled automatically from your ASPX page and, finally, you'll end up in my token dump DLL, which will show information from the token of the calling process and thread. The thread information will only be present if the thread is actually impersonating, so if you only see information for a process token, you'll know that the thread was not impersonating when it called TokenDump.

Figure 4 Results without Impersonation
Figure 4 Results without Impersonation

Figure 4 shows the results prior to impersonation, and Figure 5 shows the results after turning on impersonation via Web.config. Note that I've left the worker process running as SYSTEM. I'll probably continue to use this setting for testing my own code until the userName='Machine' bugs are ironed out.

Figure 5 Results with Impersonation
Figure 5 Results with Impersonation

CLR Threads versus OS Threads

      So far, even though I've been talking about building ASP .NET applications that are built with managed code, I've been completely focused on the unmanaged aspects of security. There's a good reason for this: I want you to internalize something here. The Common Language Runtime (CLR) has a completely separate security infrastructure which layers on top of whatever operating system you happen to be using. If you're running on an operating system based on Windows NT®, you'll have OS processes and threads that have tokens attached to them. You'll be subject to the security policy of the OS when you attempt to open files, and so on. Running in the CLR doesn't exempt you from that.
      However, what if you're running on Windows 98, or for that matter, some mobile device? These systems have no security infrastructure. You can open any file you like on Windows 98; just go for it. The CLR security infrastructure still exists on these platforms. Code access security and principal-based security still function because they are completely abstracted from the underlying operating system.
      This abstraction means you need to pay attention to two security contexts on secure operating systems, not just one. Let me demonstrate by extending this example (see Figure 6).
      Now I will configure IIS to reject unauthenticated callers, forcing myself to be authenticated using Integrated Windows authentication. I'll also add a line to my web.config file, so that I can explicitly control how ASP .NET performs authentication:

  <configuration>
  
<system.web>
<identity impersonate='true'/>
<authentication mode='None'/> <!-- new -->
</system.web>
</configuration>

 

      Figure 7 shows the results. Note that while my physical thread has a token for a real, authenticated user, my CLR thread has no concept of any security identity. Granted, I needed to add a line of code to my web.config file to make this work, but this is only to override a default setting in machine.config, which forces synchronization of the two identities.

Figure 7 Two Distinct Security Contexts
Figure 7 Two Distinct Security Contexts

      Adding the following code to my Web page after the import directives, I can further show the two distinct security contexts:

  <%
  
IIdentity alice = new GenericIdentity("alice");
Thread.CurrentPrincipal =
new GenericPrincipal(alice, null);
%>

 

      Figure 8 shows the results. You really need to get used to seeing this sort of thing, even though it may seem disconcerting. In this case, the CLR sees the thread running as alice, so any managed code that cares about security principals will see her as the principal. Note that the kind of code I'm talking about here will likely be your own application code because not many of the classes in the .NET Framework rely on principal-based security. What's interesting here is that as soon as the thread calls out to unmanaged code, that code will see kbrown making the call.

Figure 8 Further Distinction of Security Contexts
Figure 8 Further Distinction of Security Contexts

      A real world example of this is when you try to open a file, say c:\temp\foo.txt, using a FileStream object. The FileStream object cares nothing about CLR principal-based security, so it won't care that the CLR thread happens to be running as alice. All it cares about is code access security (CAS), so it will demand that all assemblies in the call stack have the right to access c:\temp\foo.txt. However, when the FileStream object goes to open the file, it must do so by making a normal operating system call just like you used to do in the good old days of Win32®. So it will call CreateFile to open a file handle, and guess which identity the operating system will see? That's right, it'll see kbrown, because the operating system knows nothing of CLR threads and principals. It only knows about tokens, and it sees a thread token with kbrown's identity. So even though the CLR thread's principal is alice, it'll only be able to open the file if CAS policy grants permission and if kbrown has been granted access via the file's security descriptor.

A Challenge: Principal of Least Privilege

      OK, that's enough mind-twisting ASP .NET security for one column. Next month I'll continue the saga and talk about the various types of authentication ASP .NET has to offer, focusing specifically on forms-based authentication. But before I go, I want to offer a challenge to you, Constant Reader.
      I've been developing for Windows for many years. In fact, I grew up on the PC platform, and I must admit that the last few years, during which I've been studying computer security, many of my ideas about software development have changed radically. The one idea that I'd like to share with you today is the myth that you must run with administrative privileges to develop code. Yes, it's a myth, and I admit wholeheartedly that I helped spread it until relatively recently in my career.
      I never really thought twice about it until I spent some time on Linux, writing one of the very first implementations of the Simple Object Access Protocol (SOAP) using one of my favorite pet languages, Perl. The development culture over on that side of the world (Unix in general, not just Linux) is so incredibly different from what we are used to on the Windows platform. On the Unix platform, rarely does a developer run as root (the equivalent of an administrator in the Windows world). This style of development is feasible because Unix, (originally built for use exclusively by engineers, students, and other tech weenies) came equipped with little utilities that made it easy to always run with the least possible amount of privilege. The one utility that we really needed to make this possible is the super user (su) or switch user utility. With this utility it is possible to run as a normal user while you write and compile your code. Think about it—who needs to be an admin to edit a text file, anyway? When you need to install your code, you su to root, temporarily elevating your privilege level by providing the root password, and install your code. You then immediately return to your normal login.
      Often when I teach a class or give a conference talk on security, I'll ask the developers how many normally run as administrators on their own machines, and every single person in the room will typically raise his or her hand. The one poor guy in the room who isn't allowed to run as an admin at this point shrinks down in his chair, hoping nobody will notice him. Interestingly enough, if I were talking to a crowd of Unix developers and I asked them the same question, the response would be completely the opposite. The poor guy shrinking down in his chair would be the one guy who thought it was a good idea to run as root all the time. Don't get me wrong, I'm not trying to say that Unix developers are so much smarter than Windows-based programmers. It's just they've grown up in a completely different culture.
      This is what I'd like to challenge you to change. For a long time now, I've been eating my own dogfood and running as a normal user on my laptop while I've been developing code. How do I install my code? How do I configure system services like IIS? I use the su utility that is built into Windows 2000. It's called runas:

  C:\temp>runas /user:gromit\administrator cmd
  
Enter password for gromit\administrator:

 

This causes a new command shell to launch, in a new logon session running as Administrator, so any program I launch from this command shell will run as Administrator. If I feel like launching something directly from the Start menu, I navigate to the item on the Start menu (say Internet Services Manager) and shift-right-click it, which brings up a context menu with a wonderful option, Run As�. Try it. I think you'll like it.
      Why am I asking you to do this? The reasons are twofold. First of all, you'll be exposing yourself to much less risk when you browse the Web, open e-mail attachments, and so on. But second, you'll be helping me change the culture of developers who use Windows. Way too much code is written that only really works smoothly when run with administrative privileges because it was written and tested in that kind of environment. Writing code that can be run in different security contexts takes a bit more thought because it will be installed by an administrator, but used by a regular user. For example, anything you write to HKEY_CURRENT_USER during the install is meaningless because the administrator will most likely never even run your program after it's installed.
      I must say that of all the software I've run, Microsoft has done a great job overall in this regard. Most of their programs (including the development environment) run quite nicely when you're not an administrator. My only gripe is the few programs that occasionally drop into a Darwinian loop when run in a non-administrative security context. ("Darwin" was the code name for the Microsoft Installer technology, and a Darwinian loop is what you encounter when the system begins to display that annoying little installation dialog over and over again, while you frantically press Cancel .)
      We currently have a culture that says, "open the security settings wide to get it working, then close it down before you ship." The problem with this approach is that it's incredibly painful to reduce permissions after you've coded an entire system without any restrictions in place. Features may need to be redesigned or removed completely. Since I've been thinking more and more about security, I'm now a firm believer in locking down the security policy early and opening it up slightly only when you find you really must. You will learn to design features with security in mind, and your customers won't have any false expectations of the features you'll be providing.
      But, best of all, you'll be building programs that make it easier for all of us to run with the principal of least privilege. If we want to change the stereotype that the world of Windows cares little for security, I strongly suspect that it's developers like you and me who need to initiate the revolution by changing our own attitudes and culture. Let's do it together!

Send questions and comments to Keith at briefs@microsoft.com.

Keith Brown works at DevelopMentor researching, writing, teaching, and promoting an awareness of security among programmers. Keith authored Programming Windows Security (Addison-Wesley, 2000). He coauthored Effective COM, and is currently working on a .NET security book. He can be reached at https://www.develop.com/kbrown.