One of the root
causes of SQL injection is the creation of SQL queries as strings that
are then sent to the database for execution. This behavior, commonly
known as dynamic string building or dynamic SQL, is one of the primary
causes of an application being vulnerable to SQL injection.
As a more secure
alternative to dynamic string building, most modern programming
languages and database access application program interfaces (APIs)
allow you to provide parameters to an SQL query through the use of
placeholders, or bind variables, instead of working directly with the
user input. Commonly known as parameterized statements, these are a
safer alternative that can avoid or solve many of the common SQL
injection issues you will see within an application, and you can use
them in most common situations to replace an existing dynamic query.
They also have the advantage of being very efficient on modern
databases, as the database can optimize the query based on the supplied
prepared statement, increasing the performance of subsequent queries.
I
should note, however, that parameterized statements are a method of
supplying potentially insecure parameters to the database, usually as a
query or stored procedure call. They do not alter the content of the
values that are passed to the database, though, so if the database
functionality being called uses dynamic SQL within the stored procedure
or function implementation it is still possible for SQL injection to
occur. This has historically been a problem with Microsoft SQL Server
and Oracle, both of which have shipped with a number of built-in stored
procedures that were vulnerable to SQL injection in the past, and it is a
danger that you should be aware of with any database stored procedures
or functions that use dynamic SQL in their implementation. An additional
issue to consider is that malicious content could have been stored in
the database at this point that may then be used elsewhere in the
application, causing SQL injection at another point in the application.
Here is an example of a
vulnerable piece of login page pseudocode using dynamic SQL. We will
discuss how to parameterize this code in Java, C#, and PHP in the
following sections.
Username = request(“username”)
Password = request(“password”)
Sql = “SELECT * FROM users WHERE username='” + Username + “' AND password='”
+ Password + “'”
Result = Db.Execute(Sql)
If (Result) /* successful login */
What Can Be Parameterized, and What Can't?
Not all dynamic SQL
statements can be parameterized. In particular, you can parameterize
only data values, and not SQL identifiers or keywords. Therefore, you
can't have parameterized statements such as the following:
SELECT * FROM ? WHERE username = 'john' SELECT ? FROM users WHERE username = 'john' SELECT * FROM users WHERE username LIKE 'j%' ORDER BY ?
Unfortunately,
a common solution presented in online forums to solve this problem is
to use dynamic SQL in the string that is then used to parameterize the
query, as in the following example:
String sql = “SELECT * FROM ” + tblName + “ WHERE user =?”;
In this case, you can end
up introducing an SQL injection issue where there previously wasn't one
by trying to parameterize a statement.
In general, if you're
trying to supply an SQL identifier as a parameter, you should look at
your SQL and how you're accessing your database first, and then look at
whether it is possible to rewrite the query using a fixed identifier.
Although it may be possible to solve this through the use of dynamic
SQL, this is also likely to adversely affect the performance of the
query, as the database will not be able to optimize the query.
|
Parameterized Statements in Java
Java provides the Java Database Connectivity (JDBC) framework (implemented in the java.sql and javax.sql
namespaces) as a vendor-independent method of accessing databases. JDBC
supports a rich variety of data access methods, including the ability
to use parameterized statements through the PreparedStatement class.
Here is the
earlier vulnerable example rewritten using a JDBC prepared statement.
Note that when the parameters are added (through the use of the various set<type> functions, such as setString), the index position (starting at 1) of the placeholder question mark is specified.
Connection con = DriverManager.getConnection(connectionString);
String sql = “SELECT * FROM users WHERE username=? AND password=?”;
PreparedStatement lookupUser = con.prepareStatement(sql);
// Add parameters to SQL query
lookupUser.setString(1, username); // add String to position 1
lookupUser.setString(2, password); // add String to position 2
rs = lookupUser.executeQuery();
In addition to the
JDBC framework that is provided with Java, additional packages are often
used to access databases efficiently within J2EE applications. A
commonly used persistence framework for accessing databases is
Hibernate.
Although it is possible to
utilize native SQL functionality, as well as the JDBC functionality
shown earlier, Hibernate also provides its own functionality for binding
variables to a parameterized statement. Methods are provided on the Query object to use either named parameters (specified using a colon; e.g., :parameter) or the JDBC-style question mark placeholder (?).
The following example demonstrates the use of Hibernate with named parameters:
String sql = “SELECT * FROM users WHERE username=:username AND” +
“password=:password”;
Query lookupUser = session.createQuery(sql);
// Add parameters to SQL query
lookupUser.setString(“username”, username); // add username
lookupUser.setString(“password”, password); // add password
List rs = lookupUser.list();
The
next example shows the use of Hibernate with JDBC-style question mark
placeholders for the parameters. Note that Hibernate indexes parameters
from 0, and not 1, as does JDBC. Therefore, the first parameter in the
list will be 0 and the second will be 1.
String sql = “SELECT * FROM users WHERE username=? AND password=?”;
Query lookupUser = session.createQuery(sql);
// Add parameters to SQL query
lookupUser.setString(0, username); // add username
lookupUser.setString(1, password); // add password
List rs = lookupUser.list();
Parameterized Statements in .NET (C#)
Microsoft .NET provides
access to a number of different ways to parameterize statements by using
the ADO.NET Framework. ADO.NET also provides additional functionality,
allowing you to further check the parameters supplied, such as by
type-checking the data you are passing in.
ADO.NET provides four different data providers, depending on the type of database that is being accessed: System.Data.SqlClient for Microsoft SQL Server, System.Data.OracleClient for Oracle databases, and System.Data.OleDb and System.Data.Odbc
for OLE DB and ODBC data sources, respectively. Which provider you use
will depend on the database server and drivers being used to access the
database. Unfortunately, the syntax for utilizing parameterized
statements differs among the providers, notably in how the statement and
parameters are specified. Table 1 shows how parameters are specified in each provider.
Table 1. ADO.NET Data Providers, and Parameter Naming Syntax
Data Provider | Parameter Syntax |
---|
System.Data.SqlClient | @parameter |
System.Data.OracleClient | :parameter (only in parameterized SQL command text) |
System.Data.OleDb | Positional parameters with a question mark placeholder (?) |
System.Data.Odbc | Positional parameters with a question mark placeholder (?) |
The following example shows the vulnerable example query rewritten as a parameterized statement in .NET using the SqlClient provider:
SqlConnection con = new SqlConnection(ConnectionString);
string Sql = “SELECT * FROM users WHERE username=@username” +
“AND password=@password”;
cmd = new SqlCommand(Sql, con);
// Add parameters to SQL query
cmd.Parameters.Add(“@username”, // name
SqlDbType.NVarChar, // data type
16); // length
cmd.Parameters.Add(“@password”,
SqlDbType.NVarChar,
16);
cmd.Parameters.Value[“@username”] = username; // set parameters
cmd.Parameters.Value[“@password”] = password; // to supplied values
reader = cmd.ExecuteReader();
The next example shows the same parameterized statement in .NET using the OracleClient provider. Note that the parameters are preceded by a colon in the command text (the Sql string), but not elsewhere in the code.
OracleConnection con = new OracleConnection(ConnectionString);
string Sql = “SELECT * FROM users WHERE username=:username” +
“AND password=:password”;
cmd = new OracleCommand(Sql, con);
// Add parameters to SQL query
cmd.Parameters.Add(“username”, // name
OracleType.VarChar, // data type
16); // length
cmd.Parameters.Add(“password”,
OracleType.VarChar,
16);
cmd.Parameters.Value[“username”] = username; // set parameters
cmd.Parameters.Value[“password”] = password; // to supplied values
reader = cmd.ExecuteReader();
The final example shows the same parameterized statement in .NET using the OleDbClient provider. When using the OleDbClient provider, or the Odbc provider, you must add parameters in the correct order for the placeholder question marks.
OleDbConnection con = new OleDbConnection(ConnectionString);
string Sql = “SELECT * FROM users WHERE username=? AND password=?”;
cmd = new OleDbCommand(Sql, con);
// Add parameters to SQL query
cmd.Parameters.Add(“@username”, // name
OleDbType.VarChar, // data type
16); // length
cmd.Parameters.Add(“@password”,
OleDbType.VarChar,
16));
cmd.Parameters.Value[“@username”] = username; // set parameters
cmd.Parameters.Value[“@password”] = password; // to supplied values
reader = cmd.ExecuteReader();
Tip
When
using parameterized statements with ADO.NET, it is possible to specify
less or more detail about the statement than I did in the preceding
example. For instance, you can specify just the name and the value in
the parameter constructor. In general, it is a good security practice to
specify parameters as I did, including the data size and type, because
this provides an additional level of coarse-grained validation over the
data that is being passed to the database.
Parameterized Statements in PHP
PHP also has a number of
frameworks that you can use to access a database. I'll demonstrate three
of the most common frameworks in this section: the mysqli package for
accessing MySQL databases, the PEAR::MDB2 package (which superseded the
popular PEAR::DB package), and the new PHP Data Objects (PDO) framework,
all of which provide facilities for using parameterized statements.
The mysqli package, available with PHP 5.x
and able to access MySQL 4.1 and later databases, is one of the most
commonly used database interfaces, and supports parameterized statements
through the use of placeholder question marks. The following example
shows a parameterized statement using the mysqli package:
$con = new mysqli(“localhost”, “username”, “password”, “db”);
$sql = “SELECT * FROM users WHERE username=? AND password=?”;
$cmd = $con->prepare($sql);
// Add parameters to SQL query
$cmd->bind_param(“ss”, $username, $password); // bind parameters as strings
$cmd->execute();
The
PEAR::MDB2 package is a widely used and vendor-independent framework
for accessing databases. MDB2 supports named parameters using the colon
character and using placeholder question marks. The following example
demonstrates the use of MDB2 with placeholder question marks to build a
parameterized statement. Note that the data and types are passed in as
an array which maps to the placeholders in the query.
$mdb2 =& MDB2::factory($dsn);
$sql = “SELECT * FROM users WHERE username=? AND password=?”;
$types = array('text', 'text'); // set data types
$cmd = $mdb2->prepare($sql, $types, MDB2_PREPARE_MANIP);
$data = array($username, $password); // parameters to be passed
$result = $cmd->execute($data);
The PDO package, which
is included with PHP 5.1 and later, is an object-oriented
vendor-independent data layer for accessing databases. PDO supports both
named parameters using the colon character and the use of placeholder
question marks. The following example demonstrates the use of PDO with
named parameters to build a parameterized statement:
$sql = “SELECT * FROM users WHERE username=:username AND” +
“password=:password”;
$stmt = $dbh->prepare($sql);
// bind values and data types
$stmt->bindParam(':username', $username, PDO::PARAM_STR, 12);
$stmt->bindParam(':password', $password, PDO::PARAM_STR, 12);
$stmt->execute();
Parameterized Statements in PL/SQL
Oracle PL/SQL offers
also the possibility of using parameterized queries in database-level
code. PL/SQL supports binding parameters using the colon character with
an index (e.g., :1).
The following example demonstrates the use of PL/SQL with bound
parameters to build a parameterized statement in an anonymous PL/SQL
block:
DECLARE
username varchar2(32);
password varchar2(32);
result integer;
BEGIN
Execute immediate 'SELECT count(*) FROM users where username=:1 and
password=:2' into result using username,password;
END;