In a recent blog post, Dancho Danchev mis-labeled a recent IIS vulnerability as a "massive SQL injection attack."
Let's be honest here. Yes, this alert needs attention. But this is not a new SQL injection vulnerability. It is simply an exploit in IIS that lets malicious users access your source code. If your database is already open to SQL injection attacks by anyone who can access the file system on your web servers, then yes, SQL injection is just waiting for the next vulnerability to your file system. However, if you protect your database server(s) from SQL injection in the first place, then no IIS vulnerability will magically become known as a SQL injection attack.
Never mind that half the IIS servers in the world probably don't even connect to SQL Server, and of the remainder, not all are vulnerable to SQL injection. The ones that are vulnerable are that way because the web developers and/or DBAs have been sloppy and allowed for practices that help make SQL injection possible.
Call it what it is; don't sensationalize it. And instead of trying to create panic, provide a little education! How do you prevent an IIS vulnerability from becoming a SQL injection attack? There are plenty of things you can do. Some of them are pretty obvious, or have been discussed previously, but I'll recap the ones on my list:
- Do not expose your SQL Server to the Internet directly
While in some cases you can't avoid this (shared database servers at a hosting provider, for example), if your server-side code yields a public address, or enough information that the public address can be easily determined, then you are opening yourself up. All someone needs is read access to your config file or ASP page in order to obtain credentials to access your SQL Server from anywhere. Talk to your network administrator about keeping SQL Server behind your firewall.
- Make your passwords strong
Ideally, your applications will use Windows authentication, but if you must use mixed authentication modes, then make sure your SQL Authentication passwords are "strong" passwords. It is very hard to be completely immune to a dictionary attack, but you can make it much more difficult by using a 16-character password with mixed case and alphanumerics, like '$QL$erver_r0ck$!', as opposed to an "easier" password like 'tweetybird.'
- Follow the principle of least privilege
Do not use sa as the login in the connection strings for your application. Use a low-privileged user that can only execute (certain) stored procedures. There is no reason someone should be able to add a query like "SELECT * FROM sys.objects" to your server-side code, or launch extended procedures like xp_cmdshell, or drop objects, because that user should not have sufficient access to do so… the application user should not be sa or db_owner. Lock down your applications, and only give them the rights they need.
Similarly, do not use a domain administrator or otherwise privileged user as the service account. This would mean that anything that runs under the context of SQL Server has free reign over your server or even entire network, using a variety of tools like extended procedures.
- Always use stored procedures, or at least parameterized statements
If you build ad hoc SQL in your applications, then you are asking for SQL injection attacks, and I strongly suggest you become familiar with using stored procedures or parameterized queries. Otherwise, all input becomes suspect, since it is very easy to use comments or semi-colons to change the meaning of queries or to append additional queries to be executed. With a query that uses strongly typed parameters, however, this technique becomes fruitless. This does not mean something like:
sql = "EXEC dbo.foo @param1 = '" & Request.QueryString("bar") & "'"
This is still vulnerable to SQL injection, because I can now call the page using ?bar=';drop table blat;–
Instead you should use a command object and pass the inputs to parameters. (This also prevents you from having to escape apostrophes in names like O'Hagan, delimit date literals correctly, etc.)
- Use TRY/CATCH to return more generic error messages
In order to prevent revealing your database structure, do not let errors like foreign key violations or other errors bubble up to the application. This just gives your potential attacker more information about your database structure than they need to have. Instead use error handling to say "That user does not exist" instead of the default error message SQL Server provides — which gives specific table and column information back to the user. If you are using ASP.Net, then you can make sure that you turn CustomErrorsMode to "On" or "RemoteOnly" and set compilation debug to "false"…
- Do not store passwords in your Users table
A lot of web applications store usernames and passwords so that their users can log in to the application. Instead of storing a password in plain text, which can then be read easily by anyone who manages to gain read access to the Users table, store a hash of the password (using MD5 or something similar). When the user attempts to login (hopefully via SSL), you use the same technique to hash their entry and compare the hashed values, instead of a clear text comparison. Even if the user has read access to the stored procedure that implements the hash, all they can do with it is try and try and try… they cannot reverse engineer the data if you use a proper hashing technique.