Tuesday, April 05, 2005

Fun with ASP.NET security and Windows 2003 SP1 breakage

Problem: you want secure database access, using a connection string like this:

<add key="DatabaseConnection" value="server=SERVER;Persist Security Info=False;database=DATABASE;Integrated Security=SSPI;"/>

Solution: First, we're running IIS6.0. So we can set up a separate Application Pool, and setup credentials for that application pool to use.

We don't want to use Impersonation, because then our connection credentials will run as the application user, which may be different for each request, which will slow database access down because we won't be able to use database connection pooling.

We don't want to use a domain account, because exploiting that account gives a free ride (and reconnaissance) to our entire network.

We do want to use a local account, with minimal rights on the Windows 2003/IIS6.0 server. We can then duplicate that account on the SQL server, assign it appropriate rights to the databases we're using (and specifically, the stored procedures), and then use pass-through authentication.

I used the ASPNET account (which will cause problems later, but they're interesting ones), though the account really doesn't matter (i.e. I did not use this account on our production server, but another one like it.) I think it's better to live dangerously on development boxes, to catch problems early. Of course, that's not all. In order for the account to be able to startup an application pool, it has to be a member of the IIS_WPG group. I didn't find that anywhere in MSDN or the KB articles, but by experimentation.

So, pick an account, add it to the IIS_WPG group, create an application pool running under that account, duplicate that account on your SQL server, set permissions to the databases and stored procedures desired.

Voila, right?

Problem #2: You want to use the Enterprise Library Data Access Application Block. So following the guidelines you write some code like this:

Database authDB = DatabaseFactory.CreateDatabase("Authentication");

DBCommandWrapper dbCommandWrapper = authDB.GetStoredProcCommandWrapper("usp_LookupUserbyLoginID");

dbCommandWrapper.AddInParameter("@kerbID", System.Data.DbType.String, requestUserID);

IDataReader reader = authDB.ExecuteReader(dbCommandWrapper);

bool records = false;

But get an error like this:

Security Exception

Description: The application attempted to perform an operation not allowed by the security policy. To grant this application the required permission please contact your system administrator or change the application's trust level in the configuration file.

Exception Details: System.Security.SecurityException: Requested registry access is not allowed.

Source Error:

Line 157:    DBCommandWrapper dbCommandWrapper = authDB.GetStoredProcCommandWrapper("usp_LookupUserbyLoginID");
Line 158:    dbCommandWrapper.AddInParameter("@kerbID", System.Data.DbType.String, requestUserID);
Line 159:    IDataReader reader = authDB.ExecuteReader(dbCommandWrapper);
Line 160:    bool records = false;
Line 161:   

Source File: \\Webdevel.caes.ucdavis.edu\wwwroot$\EligibilityList\AuthenticationModule.cs    Line: 159

Stack Trace:

[SecurityException: Requested registry access is not allowed.]
   Microsoft.Win32.RegistryKey.OpenSubKey(String name, Boolean writable) +473
   System.Diagnostics.EventLog.CreateEventSource(String source, String logName, String machineName, Boolean useMutex) +443
   System.Diagnostics.EventLog.WriteEntry(String message, EventLogEntryType type, Int32 eventID, Int16 category, Byte[] rawData) +347
   System.Diagnostics.EventLog.WriteEntry(String message, EventLogEntryType type, Int32 eventID, Int16 category) +21
   System.Diagnostics.EventLog.WriteEntry(String message, EventLogEntryType type, Int32 eventID) +15
   Microsoft.Practices.EnterpriseLibrary.Common.Instrumentation.EventLogger.Log(String message)

If you look at the Stack Trace, you can see the problem is with the CreateEventSource() call. Even though you haven’t specified using the Enterprise Library Logging Block, secretly it is still using System.Diagnostics.EventLog as part of its setup.

Here’s an article which describes the problem:

PRB: “Requested Registry Access Is Not Allowed” Error Message When ASP.NET Application Tries to Write New EventSource in the EventLog

Unfortunately, the solutions don’t work. Solution #1, manually entering a registry key, didn’t work for me. Solution #2, writing some code which calls CreateEventSource() also doesn’t quite work either.

I say quite because the issue is that CreateEventSource() needs to be called by a user with Administrative Rights. So what I did was create a project using my ErrorHandler class (from Fun with Microsoft’s Enterprise Library), setup the project to run in App Pool #1 which runs using the ASPNET account, grant that account Admin rights, do iisreset && gpupdate /force, open the project’s default web form thereby causing an event to be written which calls the ErrorHandler class which calls CreateEventSource(), and then go back and revoke admin rights on ASPNET.

Unfortunately, this needs to be done for every application which will call CreateEventSource() — unless you want to leave ASPNET running as Administrator (very bad idea!).

Inelegant, but it works. I’ve notified Microsoft KB site of my findings; perhaps they’ll revise the article, or show something more elegant.

Update: This is also discussed in the Enterprise Library FAQ. However, the solutions given there are 1) run the “Install Services” script (why would you install Visual Studio on a server?) 2) use installutil on the EL assemblies (perhaps that will work — I’ll have to try it) or 3) remove all logging from the EL (which in my case I want).

Okay, we’ve got that problem taken care of. We write our EL application and breathlessly open the default page, only to find:

 Server Error in '/EligibilityList' Application.
--------------------------------------------------------------------------------

File or assembly name ko20f8cc.dll, or one of its dependencies, was not found.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.IO.FileNotFoundException: File or assembly name ko20f8cc.dll, or one of its dependencies, was not found.

Source Error:


Line 119:      private bool Authorize(string requestUserID)
Line 120:      {
Line 121:         Database authDB = DatabaseFactory.CreateDatabase("Authentication");
Line 122://         IDataReader dataReader;
Line 123:         DBCommandWrapper dbCommandWrapper = authDB.GetStoredProcCommandWrapper("usp_LookupUserbyLoginID", new SqlParameter("@kerbID", requestUserID));

This was discussed in the GotDotNet forums. The problem is this:

Process and request identity in ASP.NET

Behind the scenes the DAAB calls XmlSerializer, which want to write a temporary assembly to run. ASPNET (or the account you’re running under) doesn’t have access to the default temp directory, C:\Windows\temp, so the assembly can’t be written and the DAAB halts. Nice.

To fix this, give the account the Application Pool runs under Full (that’s right, it needs to create subdirectories) permissions to C:\Windows\temp.

By the way, this use of XmlSerializer has performance implications.

Finally, Enterprise Library is installed, our code references it correctly, temporary assemblies can be written locally, life is good.

Then we install Windows Server 2003 Service Pack 1.

And instantly, our web pages return the very lonely:

Service Unavailable

Looking at IIS Manager, you can see that the Application Pool has been disabled. Looking in the System Log from Event Viewer shows this:

A failure was encountered while launching the process serving application pool 'AppPool #1'. The application pool has been disabled.

For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.

Of course that link leads to no further information.

To cut to the chase, the problem is that Windows Server 2003 SP1 has revoked rights/permissions on the ASPNET account, that cannot be restored even by placing it in the Administrators group. The way to fix the problem is:

Go to the .NET Framework Folder (typically c:\Windows\Microsoft.NET\Framework\v1.1.4322)

aspnet_regiis -ua to uninstall the framework

aspnet_regiis -i to reinstall the framework

In IIS Manager:

Enable ASP.NET pages

In User manager (compmgmt.msc)

Set the ASPNET account with the password on the SQL server, and as a member of IIS_WPG

In IIS Manager:

Set the Application pool to run under the account with the password entered from the previous step

At the Run command:

iisreset to reset IIS

gpupdate /force to ensure password synchronization

Wasn’t that fun?

Thank goodness Whidbey and Enterprise Library v2.0 aren’t coming out until September.

No comments: