How Deep are your Defenses?
So, you have built your secured web application. You have enabled ASP.NET’s handy authentication and authorization features. But have you done enough? No, not at all. What happens if you forget to deploy the web.config controlling access to the application’s administrative folder? Or if an attacker gains access to the box by exploting your database and references your business logic layers? Or if an attacker finds a SQL injection and starts writing directly to the database? In many cases, the short answer is “bad things” oftentimes leading to unemployment.
But it need not be so easy for an attacker. There are a number of tactics one can use to extend security beyond the web interface. Like a good army, you must practice defense in depth in order to protect the application.
As noted above, hardening the surface is easy—ASP.NET provides some very robust and flexible authentication and authorization features. And with version 2.0 it even provides some limited end-to-end solutions in the MembershipProviders and RoleProviders. But that just protects the surface—much like an eggshell, your application is hardish on the outside but very easily penetrable beyond the shell. Rather than relying upon the shell to provide security, you should implement a defense in depth to mitigate any effects of the shell being cracked.
Diving into the Business Logic
Far and away the easiest way to defend the Business Logic Layer is to take advantage of the .NET Framework’s security feature, particularly the PrincipalPermissionAttribute. This attribute can be applied to any class, method, property. If the Principal Permission check fails, a SecurityException will be thrown. For example, lets say you have a Customer object which should not be accessed by an unauthenticated user. You could simply decorate the object with a principal permission like so:
[PrincipalPermission(SecurityAction.Demand, Authenticated=true)]
public class Customer
{
//code for your customer object.
}
Or, lets say you had an application where the requirements state that only administrators should be able to add users to roles. A feature you encapsulated in the AddRole(string) method. You could make certain no user could, say, add themselves to the Admins group, with code like this:
[PrincipalPermission(SecurityAction.Demand, Role="Admins")]
public void AddRole(string role)
{
//code to add the role
}
In addition to providing an easy way to backstop all security, the IPrincipal interface is very useful for situations such as logging and auditing user actions. Just capture a reference to the principal object where necessary and use the IPrincipal.Identity.Name property to capture just whom is doing what to what.
There is one other massive advantage to using techniques like the above to embed security into your business logic libraries: security becomes much more testable. Testing a web interface is generally a manual job, whereas there are a number of unit testing frameworks available for .NET—such as NUnit or MbUnit—which allow for very quick and easy tests of PrincipalPermissions. All you need do is to replace the System.Threading.Thread.CurrentPrincipal with a properly constructed GenericPrincipal and look for SecurityExceptions as appropriate. And, with NUnit GUI, you can even take a pretty screenshot for the boss to prove it.
Beyond the Business Logic and Into the Data
The final line of defense in most web applications lies in the underlying datastore, typically a RDBMS of some sort. With ASP.NET, more often than not, that data store is a Sql Server. In one of those accidents of history, Sql Server is capable of being very, very secure but oftentimes the floodgates are left open by a combination of administrative and developer error. And, especially with regards to Sql Server 2000, this can lead to very, very bad things. Look at this video [56k beware] for what the combination of a misconfigured Sql Server installation and a poorly-coded website can do for you.
Insofar as securing Sql Server, there are a few basic steps that need to be done on installation. The principal one is to make certain the database is not running in the LOCAL SYSTEM context but rather as a restricted user account. This limits the damage that can be done in worst case scenarios. But this task is oftentimes well beyond the responsibility of the developer and as such cannot quite be relied upon.
From an application development perspective, there are many things that can be done to prevent Sql Injection scenarios from ever taking place in your ASP.NET applications. First and foremost, make sure to restrict your web application’s user accounts. There are absolutely no scenarios where the web application’s user account needs db_owner rights; if it requires full read/write access on all tables, use db_datareader and db_datawriter—though one could argue that all INSERTs/UPDATEs/DELETEs should take place through stored procedures. But under no circiumstances that I have seen has it been justified to give a web application rights to control access to a database. Nevermind some scenarios where pure developer laziness required the web application run with fully sysadmin rights.
Second, and possibly more important, one should always take advantage of ADO.NET parameters to pass data to the database. For example, this is bad:
public void badSql(SqlConnection conn, string input)
{
string sql = "SELECT * FROM Beers WHERE Type='{0}'";
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = string.Format(sql, input);
//execute command
}
And this is how the same method should be coded:
public void goodSql(SqlConnection conn, string input)
{
string sql = "SELECT * FROM Beers WHERE Type=@Type";
SqlParameter param = new SqlParameter("@Type", SqlDbType.VarChar);
param.Value = input;
DbCommand cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.Add(param);
//execute command
}
In addition to defending against Sql Injection, using parameterized statements is easier for your database server to digest and cache.
Finally, there is one more option for defending the database: using separate security contexts for access. For example, I develop many applications that have three front-ends: a public web application, which needs to SELECT a lot and INSERT very rarely. An application to manage the website, which needs full access to the database. And finally a web service application which needs to do a fair amount of both SELECTing and INSERTing, but only in limited ways. In the database server, I assign each application a different role and allow each of them access to the stored procedures and tables which they need. Meaning that, should I leave a data access method exposed which should not be exposed, it will still fail because it cannot access the data store.
Made it this far? First, go and secure your web applications from end to end. Second, remember to kick this post. And last, make sure to check out the handy ASP.NET security resources Scott Guthrie posted (scroll down to the bulleted lists).