Preventing SQL Injections in ASP

Author: Bala Neerumalla, Microsoft Corporation

SQL Injection attacks have drawn a lot of attention in the last few months [1]. While there has been a lot of information on best practices to prevent SQL Injection vulnerabilities in ASP.NET and TSQL, there is comparatively very little information on preventing them in ASP code. This paper discusses the common coding mistakes in ASP code that can lead to SQL Injections, the best practices to prevent them and ways to detect these vulnerabilities.

1. SQL injection issues in ASP code

When you combine untrusted user input and code, you need to be careful about injection issues. If you embed user supplied data within SQL statements then you can run into SQL Injection issues and similarly if you embed user supplied data with HTML script, then you can run into HTML scripting issues, commonly referred to as XSS, Cross Site Scripting issues.

There are two common kinds of SQL injection issues:

  • First-order SQL Injection and
  • Second-order SQL Injection

If the user-supplied data comes from ASP’s Request.Form or Request.Querystring and that data is used to construct dynamic SQL statements without any data validation, then attackers can inject SQL commands into the SQL statement and misuse it. These are generally referred to as a First Order SQL Injection vulnerability.

If user input is stored in a database via one ASP page and then retrieved from the database and used to construct dynamic SQL statements in a different ASP page, this is referred to as a Second Order SQL Injection vulnerability.

Let’s look at some examples that demonstrate these two kinds of SQL injection vulnerabilities.

First Order SQL injection issues

As I mentioned in the previous section, first-order SQL injection vulnerabilities exist when the user supplied data is directly used in the construction of dynamic SQL statement. Let’s have a look at the following example

strAuthor = Request.QueryString("authorname")
strConnectString = "Provider=SQLOLEDB; Data Source=sqlmac;Initial Catalog=test; Integrated Security=SSPI;"
Set objConn = Server.CreateObject("ADODB.CONNECTION")
Set objCommand = Server.CreateObject("ADODB.COMMAND")
' Connect to SQL Server
objConn.Open(strConnectString)
' Execute the command
strCmd = "select title, description from books where author_name = '" & strAuthor & "'"
Set objCommand.ActiveConnection = objConn
objCommand.CommandText = strCmd
objCommand.CommandType = adCmdText
Set objRS = objCommand.Execute()
' Process the resultset
' Close the objects

If you observe the code, author name is read from a GET parameter (through Request.QueryString) and is then used directly in the construction of a SQL statement (sqlCmd) without any input validation. If the author name is some valid string such as Michael then the query returns the books authored by Michael. But what if the end user supplies the author name as Michael'; drop table books;--? In this case the final SQL command becomes select title, description from books where author_name='Michael';drop table books;-- ' This not only selects the books authored by Michael but also drops the table named books. The problem here is that the developer simply assumed that the input is going to be a string without any single quotes but never validated the input string for that. In fact this code doesn’t work if the author name is some thing like O'Neil.

The best way to fix this problem is to use parameterized queries. In the previous example, assuming that the maximum length of an author name at the database is 50. You can fix the code as below

' Execute the command
strCmd = "select title, description from books where author_name = ?"
Set objCommand.ActiveConnection = objConn
objCommand.CommandText = strCmd
objCommand.CommandType = adCmdText
Set param1 = objCommand.CreateParameter ("author", adWChar, adParamInput, 50)
param1.value = strAuthor
objCommand.Parameters.Append param1
Set objRS = objCommand.Execute()
' Process the resultset
' Close the objects

As you can see, author name value is now passed to SQL Server as a parameter. Since the value is not used in the construction of a SQL statement, there is no scope for injection issues.

Let’s look at another example that gets the user input from Request.Form instead of Request.QueryString.

strUserName   = Request.Form("username")
strPassword = Request.Form("password")
strConnectString = "Provider=SQLOLEDB; Data Source=sqlmac;Initial Catalog=test; Integrated Security=SSPI;"
Set objConn = Server.CreateObject("ADODB.CONNECTION")
Set objCommand = Server.CreateObject("ADODB.COMMAND")
' Connect to SQL Server
objConn.Open(strConnectString)
' Execute the command
strCmd = "select user_id, user_name from users where user_name='" + strUserName + "' AND password='" + strPassword + "'"
Set objCommand.ActiveConnection = objConn
objCommand.CommandText = strCmd
objCommand.CommandType = adCmdText
Set objRS = objCommand.Execute()
' Process the resultset
' Close the objects

Just like the previous example, the username and password values received from the end user are used in the construction of a dynamic SQL statement without any input validation. This can easily be exploited by entering username as admin'— and a dummy password and make the final sql statement as select user_id, user_name from users where user_name='admin';--' AND password='dummy'. The only difference is that the values are read from Request.Form. Just like the previous example, this can easily be fixed using parameterized queries as shown here.

' Execute the command
strCmd = "select user_id, user_name from users where user_name=? AND password=?"
Set objCommand.ActiveConnection = objConn
objCommand.CommandText = strCmd
objCommand.CommandType = adCmdText
Set param1 = objCommand.CreateParameter ("user", adWChar, adParamInput, 20)
param1.value = strUserName
objCommand.Parameters.Append param1
Set param2 = objCommand.CreateParameter ("password", adWChar, adParamInput, 50)
param1.value = strPassword
objCommand.Parameters.Append param2
Set objRS = objCommand.Execute()
' Process the resultset
' Close the objects

Second Order SQL injection issues

Now that we have looked at few first order SQL injection issues, let’s take a look at the following example where the data comes from the database.

strBookID = Request.QueryString("bookid")
strConnectString = "Provider=SQLOLEDB; Data Source=sqlmac;Initial Catalog=test; Integrated Security=SSPI;"
Set objConn = Server.CreateObject("ADODB.CONNECTION")
Set objCommand = Server.CreateObject("ADODB.COMMAND")
Set objCommand1 = Server.CreateObject("ADODB.COMMAND")
Set rsBook = Server.CreateObject("ADODB.RECORDSET")
Set rsBooks = Server.CreateObject("ADODB.RECORDSET")
' Connect to SQL Server
objConn.Open(strConnectString)
' Execute the command
strCmd = "select title, description, author_name from books where book_id=?"
objCommand.CommandText = strCmd
objCommand.CommandType = adCmdText
Set objCommand.ActiveConnection = objConn
Set param1 = objCommand.CreateParameter ("id", adInteger, adParamInput)
param1.value = strBookID
objCommand.Parameters.Append param1
' Open the recordset, another way of executing SQL statements
rsBook.Open objCommand
' Process the resultset
If rsBooks.EOF = False Then
    ' Get other books authored by the same person
    strAuthorName = rsBook.Fields("author_name")
    strCmd = "select * from books where author_name='" + strAuthorName + "'"
    ' Execute this query and process the results
    Set objCommand1.ActiveConnection = objConn
    objCommand1.CommandText = strCmd
    objCommand1.CommandType = adCmdText
    rsBooks.Open objCommand1
    ' Process the books
    rsBooks.MoveNext
End If
' Close the objects

You will notice that in the prior code example I used the Open method of the RecordSet object to execute dynamic SQL. This is another way of executing dynamic SQL in ASP. Let’s look at how the SQL statements are constructed. The first statement, select title, description, author_name from books where book_id=? is a parameterized query as the user input bookid is not used in the construction of the dynamic SQL statement. So there is no SQL injection issue there. But if you look at the second statement, the code uses author_name value which is read from the books table. This can result in a SQL injection issue if an untrusted user controls this data through some other ASP page. If the web site has another page which lets end users to enter book details, then a bad guy can provide some malicious author name like dummy’; <sql statement of his choice>-- and misuse this vulnerability.

The fix is similar to the first two examples. Since this is a simple SELECT statement, you can use parameterized SQL statement to mitigate the second order SQL injection issue.

If rsBooks.EOF = False Then
    ' Get other books authored by the same person
    strAuthorName = rsBook.Fields("author_name")
    strCmd = "select * from books where author_name=?"
    ' Execute this query and process the results
    Set objCommand1.ActiveConnection = objConn
    objCommand1.CommandText = strCmd
    objCommand1.CommandType = adCmdText
    Set authParam = objCommand.CreateParameter ("author", adWChar, adParamInput, 50)
    authParam.value = strAuthorName
    objCommand1.Parameters.Append authParam
    rsBooks.Open objCommand1, objConn
    ' Process the books
    rsBooks.MoveNext
End If

Even if the author_name value is validated before inserting it into the database, you should use parameterized SQL queries as a defense in depth step.

Other SQL Injections

Now that we have seen the most common sql injection issues in ASP, let’s see another example where user supplied data is used in the derivation of a table name.

strUserName   = Request.Form("username")
strPassword = Request.Form("password")
strConnectString = "Provider=SQLOLEDB; Data Source=sqlmac;Initial Catalog=test; Integrated Security=SSPI;"
Set objConn = Server.CreateObject("ADODB.CONNECTION")
Set objCommand = Server.CreateObject("ADODB.COMMAND")
' Connect to SQL Server
objConn.Open(strConnectString)
' Execute the command
strCmd = "select user_id, user_name from users where user_name=? AND password=?"
Set objCommand.ActiveConnection = objConn
objCommand.CommandText = strCmd
objCommand.CommandType = adCmdText
Set param1 = objCommand.CreateParameter ("user", adWChar, adParamInput, 20)
param1.value = strUserName
objCommand.Parameters.Append param1
Set param2 = objCommand.CreateParameter ("password", adWChar, adParamInput, 50)
param1.value = strPassword
objCommand.Parameters.Append param2
Set objRS = objCommand.Execute()
If objRS.EOF = True Then
    ' Sorry user name doesn't exist with these credentials.
End If
' Get users mails from his table.
strCmd = "select * from " + strUserName + "_emails"
Set objEmails = objConn.Execute strCmd(strCmd)
' Process the emails

A quick look at the above code snippet shows that the web page verifies a user account and then displays emails related to that user. It looks like there is one table for each user that is derived from the user’s name. So the variable in the second SELECT statement is actually the table name. While you write parameterized SQL queries that use varying values as column data, you cannot use parameterized queries for object names, such as table and column names. You can fix potential SQL injection vulnerabilities of this nature by using proper delimiters and escaping characters. For Microsoft SQL Server object identifiers, you must enclose the object names in square brackets and replace all the occurrences of right square brackets with two right square brackets. This is explained in detail at  https://msdn.microsoft.com/en-us/magazine/cc163523.aspx [2].

Here is how you fix it.

strCmd = "select * from [" + Replace(strUserName, "]", "]]") + "_emails]"
Set objEmails = objConn.Execute strCmd(strCmd)

So if the user name contains any spaces or other special characters they will all be treated as part of a table name and there won’t be any syntax errors or SQL injection opportunities. For example, if the username is some thing like admin_emails; drop table admin_emails; --, then the whole statement will become select * from [admin_emails; drop table admin_emails; --_emails]. SQL Server would treat the expression admin_emails; drop table admin_emails; --_emails as one table name because the whole string is enclosed inside square brackets. One note of caution, there should not be any data transformations done after the escaping. If you escape the string and then do some valid unencoding then that can lead to SQL Injections. So make sure you perform all valid unencoding or validation prior to calling the Replace method.

You may have noticed I used the Execute method of the Connection object in the previous example. This is the third way of executing dynamic SQL in ASP code.

2. Identifying SQL Injection issues in ASP code

As I mentioned in the previous section, there are three methods that let you execute SQL statements in ASP.

  • Using the Execute() method of the ADODB.Command object
  • Using the Open() method of the ADODB.RecordSet object
  • Using the Execute() method of the ADODB.Connection object

Use the following steps to identify SQL injection issues in your source code.

  • Identify all the instances of the above mentioned methods and any wrapper functions for these methods in your source code.
  • For each instance of these function calls, observe the way the SQL statement is constructed. Carefully review to find out if the user supplied data is used in the query construction either directly or indirectly.

3. Mitigating SQL Injection issues in ASP

Follow these guidelines to mitigate SQL injection issues in ASP code

  • Validate user input. Use RegExp.Test() method[3] to reject all invalid input.
  • Use parameterized queries for data manipulation language (DML) statements (SELECT/INSERT/UPDATE/DELETE).
  • For SQL object names, escape right square brackets and delimit them with square brackets.
  • For string literals inside data definition language (DDL) statements, escape single quotes and delimit them in single quotes.

References

[1] https://blogs.technet.com/msrc/archive/2008/04/25/questions-about-web-server-attacks.aspx - Mass SQL injection attacks

[2] https://msdn.microsoft.com/en-us/magazine/cc163523.aspx - New SQL Truncation Attacks And How To Avoid Them

[3] https://msdn.microsoft.com/en-us/library/y32x2hy1(VS.85).aspx – RegExp.Test() method

[4] https://www.microsoft.com/mspress/books/5957.aspx - Writing Secure Code by Michael Howard and David LeBlanc