Cover V14, i05

Article

may2005.tar

Avoiding SQL Injection Attacks

Randal L. Schwartz

In recent months, the entertainment news was abuzz with how socialite Paris Hilton had had her Sidekick phone "hacked", revealing phone numbers for many famous people, interesting notes about possible profit participation in the "stolen" sex tape, and lurid pictures, all of which were broadcast widely on the Internet. But the quiet word amongst computer security professionals was that this wasn't a matter of hacking into her phone at all, not that the entertainment news people would know or care. Instead, certain individuals have claimed responsibility through a well-known security flaw on T-Mobile's Web site, with which the phone is synchronized. Thus, it wasn't the phone that was the source of information -- it was the Web site.

While the precise steps by which the intruder obtained access may never be known, one of the most common intrusion paths is the SQL Injection Attack. An intruder guesses either by trial and error or determines by examining the source code of the application some field in a form is being used to construct an SQL query. Unless the application carefully verifies the content of this field, or quotes the unusual characters properly, it might be possible for the field's value to escape beyond its intended use and create additional SQL constructs.

Let's look at a specific example. Suppose a bank Web application is trying to show all the bank account balances that belong to me. The SQL might look like:

SELECT account_id, balance
FROM account_data
WHERE account_owner = '[ME]';
Here, the [ME] comes from some other part of the calculation, which isn't relevant. The effect is that I see only my data. Now, let's say that the Web programmers at the bank upgrade the application so that I can distinguish my personal and business accounts via a pop-up form element, which returns back a "1" for personal and "2" for business. So, they change this SQL to be:

SELECT account_id, balance
FROM account_data
WHERE account_owner = '[ME]'
AND account_type = $account_type;
Of course, they presume that the values "1" and "2" will drop in quite nicely there, and the application tests out just fine, so the programmers move on to the next item in their long to-do list.

What many Web programmers fail to realize is that just because a pop-up form specifies a list of values or a text field provides a maximum length (even enforced by JavaScript), this doesn't mean that a user can't construct an arbitrary value for any returned form parameter. Of course, changing this value to "3" won't do me much good. I won't get anything at all because there aren't any accounts with a type of 3.

But suppose I jimmy up a return value so that $account_type comes back as: 1 OR 99 = 99. Let's fill that in:

SELECT account_id, balance
FROM account_data
WHERE account_owner = '[ME]'
AND account_type = 1 OR 99 = 99;
Oops! What just happened? Since the OR has a lower precedence than the AND, we're saying essentially "true" for every row of the table. Presuming we have the patience to page through the result, we now have everyone's account numbers and balances. Oops.

There are many other variants on SQL injection attacks. You might be rather shocked when you begin a Web search for further information. But let's cut to the right way to do this -- with DBI placeholders. The DBI interface permits a chunk of SQL to contain question marks wherever data is being used. For example, the SQL above could be written as:

my $sth = $dbh->prepare(q{
  SELECT account_id, balance
  FROM account_data
  WHERE account_owner = ?
  AND account_type = ?
});
This takes an existing database handle (here, $dbh), and creates a statement handle ($sth) that is safely protected from SQL injection attacks. We use this handle by calling execute, providing the values to be inserted:

$sth->execute('ABC 123', 2);
The DBI system goes through the data, figuring out how to quote the values properly for the corresponding database. For example, the statement being executed will likely end up being something like:

SELECT account_id, balance
FROM account_data
WHERE account_owner = 'ABC 123'
AND account_type = 2
Note that the string was enclosed in single quotes, but the number remained unquoted. (This is important for databases that respect data types.) Now, let's see what happens if we try to pass that complex string as the account type number:

$sth->execute('ABC 123', '1 OR 99 = 99');
This results in the effective SQL of:

SELECT account_id, balance
FROM account_data
WHERE account_owner = 'ABC 123'
AND account_type = '1 OR 99 = 99'
Ahh, look! The quotes enclose the now-apparent string value. While the query is likely to fail (no account type will match that), we didn't get to see everyone's account balances either. Success.

The execute method should get one value for every placeholder in the query string. The undef value is properly mapped as NULL, while numbers and strings pass through properly quoted for the active database. Certain types may require hinting as to the actual data type; see the DBI documentation for further information.

One common problem when using placeholders is that we must maintain a correlation between chunks of SQL and the actual values we want to plug in. Good discipline helps here, but there's a nice module in the CPAN called SQL::Abstract that helps us say what we intend with a bit more clarity.

One feature of SQL::Abstract is that a particular WHERE clause can be represented by relatively straightforward complex data structure. For example, AND-ing two conditions is represented by a hashref:

{ account_owner => $owner, account_type => $account_type }
Here, $owner contains our ABC 123 and $account_type is 1 or 2.

Using SQL::Abstract, we can convert this to the SQL to feed to prepare, and the bind parameters to feed to execute with one simple step:

use SQL::Abstract;
my $sqa = SQL::Abstract->new;

my ($owner, $account_type) = ('ABC 123', 2); # from inputs

my ($sql, @bind) = $sqa->select(
  'account_data', # table
  [qw(account_id balance)], # fields
  {
   account_owner => $owner,
   account_type => $account_type
  }, # "where"
);

my $sth = $dbh->prepare_cached($sql); # reuse SQL if we can
$sth->execute(@bind);                 # execute it for this query
The result is a good query, with good bind values in the right order. And we're still safe from SQL injection attacks. If we also wanted to add account_status as not in suspended or deleted, we could change the query to:

{
  account_owner => $owner,
  account_type => $account_type,
  account_status => { -not_in => ['suspended', 'deleted'] },
}
In this case, the corresponding SQL looks like:

SELECT account_id, balance
FROM account_data WHERE
( account_owner = ?
  AND account_status NOT IN ( ?, ? )
  AND account_type = ? )
with bind values of:

'ABC 123', 'suspended', 'deleted', 2
Note that the ordering of the hash elements affects the output, but the bind values did indeed line up with the corresponding placeholders.

I won't repeat the entire manpage for SQL::Abstract here, but it looks sufficient for most of the queries I've ever wanted to create in SQL.

I can even use SQL::Abstract with my Class::DBI-wrapped databases using Class::DBI::AbstractSearch. Mixing in this class to my Class::DBI-derived base class gives me the search_where clause, accepting that same data structure. For example:

my @results = My::DB::AccountData->search_where({
  account_owner => $owner,
  account_type => $account_type,
  account_status => { -not_in => ['suspended', 'deleted'] },
});
And there, no SQL to write at all, again, and I'm safe from SQL Injection Attacks.

I hope you find SQL::Abstract useful, and never have your name appear in a headline regarding a bad security break-in. Until next time, enjoy!

Randal L. Schwartz is a two-decade veteran of the software industry -- skilled in software design, system administration, security, technical writing, and training. He has coauthored the "must-have" standards: Programming Perl, Learning Perl, Learning Perl for Win32 Systems, and Effective Perl Programming. He's also a frequent contributor to the Perl newsgroups, and has moderated comp.lang.perl.announce since its inception. Since 1985, Randal has owned and operated Stonehenge Consulting Services, Inc.