Learn About SQL Injection Attacks
By: Vickie Li
Tags: Cybersecurity, Programming, PHP, Software Development, Hacking

Preventing SQL Injections

You can see that SQL injection is a severe vulnerability that you should prevent. Let’s now talk about how you can prevent them in your web applications.

You might have heard of several SQL injection prevention techniques, like prepared statements, approval listing, typecasting, and escaping. But what are the pros and cons of each option, and when should you use them?

Prepared statements Approval listing Typecasting Escaping

Prepared statements

First, let’s talk about your best option: prepared statements. Prepared statements are also called parameterized queries, and they make SQL injections virtually impossible.

Before we dive into how prepared statements work, it’s important to understand how SQL queries are executed. SQL is essentially a programming language, and your SQL query is essentially a program. When the SQL program arrives at the SQL server, the server will parse, compile, and optimize it. Finally, the server will execute the program and return the results of the execution.

And when you insert user-supplied input into your SQL queries directly, you are basically rewriting your program dynamically using user input. So an attacker can supply data that interferes with the program’s code and alter its logic.

And prepared statements work by making sure that user-supplied data does not alter your SQL query’s logic. Prepared statements are SQL statements sent to and compiled by the SQL server before any user-supplied parameters are inserted. This means that instead of passing a complete SQL query to the server to be compiled, you define all the SQL logic first, compile it, then insert parameters into the query right before execution.

After the parameters are inserted into the final query, the query will not be parsed and recompiled. Anything that was not in the original statement will be treated as string data, not executable SQL code. So the program logic part of your SQL query will remain intact. This allows the database to distinguish between the code part and the data part of the SQL query, regardless of what the user input looks like. For example, here’s how you would implement a prepared statement in PHP.

<?php

$stmt = $mysqli->prepare(“SELECT Id FROM Users WHERE Username=? AND Password=?”);

$stmt->bind_param(“ss”, $username, $password);

$stmt->execute();

?>

In a prepared statement, you would define the structure of the query first. You write out the query without its parameters and put question marks as place holders for the parameters. The SQL server will then compile this string as SQL code. You can then send over the parameters of the query separately. The ss here means that we are providing two parameters, which are both strings. And finally, you execute the query.

Something else to remember here is that using prepared statements does not necessarily mean that you are safe from SQL injections. You also have to use them correctly. For example, here let’s say we concatenate the user input within the prepare function, then execute it right away:

<?php

$stmt = $mysqli->prepare(“SELECT Id FROM Users WHERE Username=’$username’ AND Password=’$password’”);

$stmt->execute();

?>

If you don’t send user input separately as parameters to your prepared statement but instead still build your SQL query by joining strings together, you are still vulnerable to SQL injections even when using prepared statements.

Approval listing

Let’s say that your program allows users to sort their emails by some criteria. And if the user sorts their emails by the date the email is sent, the application will execute this SQL query to retrieve the emails.

SELECT Title, Body FROM Emails WHERE Username=’vickie’ AND AccessKey=’mykey_123' ORDER BY Date DESC;

The SQL ORDER BY clause allows the query to specify which column to order the results by. This query will return all of the user’s emails in our table sorted by the Date column in descending order.

But you want your users to be able to choose which field their emails are sorted by. In this case, you cannot use prepared statements to secure the query. Prepared statements can only be used to protect fields that are not needed during the compilation process. You cannot use prepared statements in column names, table names, SQL operators, or within an ORDER BY clause. So if you need to use user-supplied input in these fields, approval listing would be the most appropriate defense against SQL injections.

Approval listing refers to only accepting input values that are known to be legit, and rejecting all other input. In this case, you can use an approval list of column names for the ORDER BY clause, instead of allowing arbitrary input from the user. Let’s say that users can only sort their emails by date or by the email's sender. You can check if the user input corresponds to one of the allowed values and then insert it into your SQL statement. Here is how you might implement the protection in PHP.

<?php

if($_POST[“order_by”] == “Date” || $_POST[“order_by”] == “Sender”) {

$order_by = $_POST[“order_by”];

}

$stmt = $mysqli->prepare(“SELECT Id FROM Emails WHERE Username=? AND AcessKey=? ORDER BY $order_by”);

$stmt->bind_param(“ss”, $username, $accesskey);

$stmt->execute();

?>

You can also map user-supplied values into predefined strings in the program to avoid concatenating user input into SQL queries. For example, if you want your users to sort their results either in an ascending or descending way, you could let users specify a boolean value that would then be mapped to a string to be inserted into the query.

<?php

if($_POST[“order_by”] == “Date” || $_POST[“order_by”] == “Sender”) {

$order_by = $_POST[“order_by”];

} $sort_by = “ASC”;

if($_POST[“desc”] == “1”) {

$sort_by = “DESC”;

}

$stmt = $mysqli->prepare(“SELECT Id FROM Emails WHERE Username=? AND AcessKey=? ORDER BY $order_by $sort_by”);

$stmt->bind_param(“ss”, $username, $accesskey);

$stmt->execute();

?>

Type casting

If your user input should be a simple data type like a boolean, a number, or a date, you could also convert the user input string into a safer data type before it is appended to the query. This is called type casting and can be used to protect against SQL injection without using prepared statements.

For example, let’s say that you also allow users to retrieve their emails with their user ID. And you know that the provided user ID should always be an integer.

SELECT Title, Body FROM Emails WHERE Id='2' AND AcessKey='mykey_123';

In this case, you can convert the user input string into an integer before you insert it into the SQL query. This will ensure that the input is a number and that no special character that can influence the SQL logic would survive the conversion.

<?php

$id = (int)$_POST[“id”];

$accesskey = $_POST[“access_key”]; $stmt = $mysqli->prepare(“SELECT Title, Body FROM Emails WHERE Id=$id AND AcessKey=?”);

$stmt->bind_param(“s”, $accesskey);

$stmt->execute();

?>

You might have noticed that we could have just used prepared statements in this example. And you are absolutely right. I recommend using prepared statements whenever possible instead of type casting because prepared statements work for all data types. This way, your coding style can remain consistent for all types of user input. The type casting method should be reserved for when prepared statements are not available.

<?php

$id = $_POST[“id”];

$accesskey = $_POST[“access_key”]; $stmt = $mysqli->prepare(“SELECT Title, Body FROM Emails WHERE Id=? AND AcessKey=?”);

$stmt->bind_param(“ss”, $id, $accesskey);

$stmt->execute();

?>

Escaping

The last way you can prevent SQL injection is to sanitize and escape user input carefully. This means that you encode or remove special characters that might mess with the SQL logic. For example, some special characters that should be sanitized or escaped include the single quote (‘), and the double quote (“), which often allows attackers to break out of a parameter string. There are also special characters specific to each type of database that you need to take care of.

This technique should only be used as a last resort, and if possible, you should not rely on it because it does not guarantee it will prevent all SQL injection in all situations. It is easy to miss out on some special characters that could construct a SQL injection attack.

Let’s look at this query again. What if you want your users to be able to sort their emails by a random field? If you cannot determine an approval list that can be used in this situation, you can first escape the user input string and then insert it into the query.