How can I prevent SQL injection in PHP?
If user input is inserted without modification into an SQL query, then the application becomes vulnerable to SQL injection, like in the following example:
$unsafe_variable = $_POST['user_input']; mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
That’s because the user can input something like value'); DROP TABLE table;--
, and the query becomes:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
What can be done to prevent this from happening?
Regarding many useful answers, I hope to add some value to this thread.
SQL injection is an attack that can be done through user inputs (inputs that filled by a user and then used inside queries). The SQL injection patterns are correct query syntax while we can call it: bad queries for bad reasons, and we assume that there might be a bad person that try to get secret information (bypassing access control) that affect the three principles of security (confidentiality, integrity, and availability).
Now, our point is to prevent security threats such as SQL injection attacks, the question asking (how to prevent an SQL injection attack using PHP), be more realistic, data filtering or clearing input data is the case when using user-input data inside such query, using PHP or any other programming language is not the case, or as recommended by more people to use modern technology such as prepared statement or any other tools that currently supporting SQL injection prevention, consider that these tools not available anymore? How do you secure your application?
My approach against SQL injection is: clearing user-input data before sending it to the database (before using it inside any query).
Data filtering for (converting unsafe data to safe data)
Consider that PDO and MySQLi are not available. How can you secure your application? Do you force me to use them? What about other languages other than PHP? I prefer to provide general ideas as it can be used for wider border, not just for a specific language.
- SQL user (limiting user privilege): most common SQL operations are (SELECT, UPDATE, INSERT), then, why give the UPDATE privilege to a user that does not require it? For example, login, and search pages are only using SELECT, then, why use DB users in these pages with high privileges?
RULE: do not create one database user for all privileges. For all SQL operations, you can create your scheme like (deluser, selectuser, updateuser) as usernames for easy usage.
See principle of least privilege.
-
Data filtering: before building any query user input, it should be validated and filtered. For programmers, it’s important to define some properties for each user-input variables: data type, data pattern, and data length. A field that is a number between (x and y) must be exactly validated using the exact rule, and for a field that is a string (text): pattern is the case, for example, a username must contain only some characters, let’s say [a-zA-Z0-9_-.]. The length varies between (x and n) where x and n (integers, x <=n). Rule: creating exact filters and validation rules are best practices for me.
-
Use other tools: Here, I will also agree with you that a prepared statement (parametrized query) and stored procedures. The disadvantages here is these ways require advanced skills which do not exist for most users. The basic idea here is to distinguish between the SQL query and the data that is used inside. Both approaches can be used even with unsafe data, because the user-input data here does not add anything to the original query, such as (any or x=x).
For more information, please read OWASP SQL Injection Prevention Cheat Sheet.
Now, if you are an advanced user, start using this defense as you like, but, for beginners, if they can’t quickly implement a stored procedure and prepared the statement, it’s better to filter input data as much they can.
Finally, let’s consider that a user sends this text below instead of entering his/her username:
[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'
This input can be checked early without any prepared statement and stored procedures, but to be on the safe side, using them starts after user-data filtering and validation.
The last point is detecting unexpected behavior which requires more effort and complexity; it’s not recommended for normal web applications.
Unexpected behavior in the above user input is SELECT, UNION, IF, SUBSTRING, BENCHMARK, SHA, and root. Once these words detected, you can avoid the input.
UPDATE 1:
A user commented that this post is useless, OK! Here is what OWASP.ORG provided:
Primary defenses:
Option #1: Use of Prepared Statements (Parameterized Queries)
Option #2: Use of Stored Procedures
Option #3: Escaping all User Supplied InputAdditional defenses:
Also Enforce: Least Privilege
Also Perform: White List Input Validation
As you may know, claiming an article should be supported by a valid argument, at least by one reference! Otherwise, it’s considered as an attack and a bad claim!
Update 2:
From the PHP manual, PHP: Prepared Statements – Manual:
Escaping and SQL injection
Bound variables will be escaped automatically by the server. The server inserts their escaped values at the appropriate places into the statement template before execution. A hint must be provided to the server for the type of bound variable, to create an appropriate conversion. See the mysqli_stmt_bind_param() function for more information.
The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements if input values are escaped correctly.
Update 3:
I created test cases for knowing how PDO and MySQLi send the query to the MySQL server when using a prepared statement:
PDO:
$user = "''1''"; // Malicious keyword $sql = 'SELECT * FROM awa_user WHERE userame =:username'; $sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY)); $sth->execute(array(':username' => $user));
Query Log:
189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\'' 189 Quit
MySQLi:
$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) { $stmt->bind_param("s", $user); $user = "''1''"; $stmt->execute();
Query Log:
188 Prepare SELECT * FROM awa_user WHERE username =? 188 Execute SELECT * FROM awa_user WHERE username ='\'\'1\'\'' 188 Quit
It’s clear that a prepared statement is also escaping the data, nothing else.
As also mentioned in the above statement,
The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements, if input values are escaped correctly
Therefore, this proves that data validation such as intval()
is a good idea for integer values before sending any query. In addition, preventing malicious user data before sending the query is a correct and valid approach.
Please see this question for more detail: PDO sends raw query to MySQL while Mysqli sends prepared query, both produce the same result
References:
Security Warning: This answer is not in line with security best practices. Escaping is inadequate to prevent SQL injection, use prepared statements instead. Use the strategy outlined below at your own risk. (Also,
mysql_real_escape_string()
was removed in PHP 7.)Deprecated Warning: The mysql extension is deprecated at this time. we recommend using the PDO extension
I use three different ways to prevent my web application from being vulnerable to SQL injection.
- Use of
mysql_real_escape_string()
, which is a pre-defined function in PHP, and this code add backslashes to the following characters:\x00
,\n
,\r
,\
,'
,"
and\x1a
. Pass the input values as parameters to minimize the chance of SQL injection. - The most advanced way is to use PDOs.
I hope this will help you.
Consider the following query:
$iId = mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string() will not protect here. If you use single quotes (‘ ‘) around your variables inside your query is what protects you against this. Here is an solution below for this:
$iId = (int) mysql_real_escape_string("1 OR 1=1"); $sSql = "SELECT * FROM table WHERE id = $iId";
This question has some good answers about this.
I suggest, using PDO is the best option.
Edit:
mysql_real_escape_string()
is deprecated as of PHP 5.5.0. Use either mysqli or PDO.
An alternative to mysql_real_escape_string() is
string mysqli_real_escape_string ( mysqli $link , string $escapestr )
Example:
$iId = $mysqli->real_escape_string("1 OR 1=1"); $mysqli->query("SELECT * FROM table WHERE id = $iId");
A simple way would be to use a PHP framework like CodeIgniter or Laravel which have inbuilt features like filtering and active-record so that you don’t have to worry about these nuances.
Warning: the approach described in this answer only applies to very specific scenarios and isn’t secure since SQL injection attacks do not only rely on being able to inject X=Y
.
If the attackers are trying to hack into the form via PHP’s $_GET
variable or with the URL’s query string, you would be able to catch them if they’re not secure.
RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+) RewriteRule ^(.*) ^/track.php
Because 1=1
, 2=2
, 1=2
, 2=1
, 1+1=2
, etc… are the common questions to an SQL database of an attacker. Maybe also it’s used by many hacking applications.
But you must be careful, that you must not rewrite a safe query from your site. The code above is giving you a tip, to rewrite or redirect (it depends on you) that hacking-specific dynamic query string into a page that will store the attacker’s IP address, or EVEN THEIR COOKIES, history, browser, or any other sensitive information, so you can deal with them later by banning their account or contacting authorities.
There are so many answers for PHP and MySQL, but here is code for PHP and Oracle for preventing SQL injection as well as regular use of oci8 drivers:
$conn = oci_connect($username, $password, $connection_string); $stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123'); oci_bind_by_name($stmt, ':xx', $fieldval); oci_execute($stmt);
A good idea is to use an object-relational mapper like Idiorm:
$user = ORM::for_table('user') ->where_equal('username', 'j4mie') ->find_one(); $user->first_name = 'Jamie'; $user->save(); $tweets = ORM::for_table('tweet') ->select('tweet.*') ->join('user', array( 'user.id', '=', 'tweet.user_id' )) ->where_equal('user.username', 'j4mie') ->find_many(); foreach ($tweets as $tweet) { echo $tweet->text; }
It not only saves you from SQL injections, but from syntax errors too! It also supports collections of models with method chaining to filter or apply actions to multiple results at once and multiple connections.
Deprecated Warning: This answer’s sample code (like the question’s sample code) uses PHP’s
MySQL
extension, which was deprecated in PHP 5.5.0 and removed entirely in PHP 7.0.0.Security Warning: This answer is not in line with security best practices. Escaping is inadequate to prevent SQL injection, use prepared statements instead. Use the strategy outlined below at your own risk. (Also,
mysql_real_escape_string()
was removed in PHP 7.)
Using PDO and MYSQLi is a good practice to prevent SQL injections, but if you really want to work with MySQL functions and queries, it would be better to use
$unsafe_variable = mysql_real_escape_string($_POST['user_input']);
There are more abilities to prevent this: like identify – if the input is a string, number, char or array, there are so many inbuilt functions to detect this. Also, it would be better to use these functions to check input data.
$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');
$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');
And it is so much better to use those functions to check input data with mysql_real_escape_string
.
I’ve written this little function several years ago:
function sqlvprintf($query, $args) { global $DB_LINK; $ctr = 0; ensureConnection(); // Connect to database if not connected already. $values = array(); foreach ($args as $value) { if (is_string($value)) { $value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'"; } else if (is_null($value)) { $value = 'NULL'; } else if (!is_int($value) && !is_float($value)) { die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.'); } $values[] = $value; $ctr++; } $query = preg_replace_callback( '/{(\\d+)}/', function($match) use ($values) { if (isset($values[$match[1]])) { return $values[$match[1]]; } else { return $match[0]; } }, $query ); return $query; } function runEscapedQuery($preparedQuery /*, ...*/) { $params = array_slice(func_get_args(), 1); $results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results. return $results; }
This allows running statements in an one-liner C#-ish String.Format like:
runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);
It escapes considering the variable type. If you try to parameterize table, column names, it would fail as it puts every string in quotes which is an invalid syntax.
SECURITY UPDATE: The previous str_replace
version allowed injections by adding {#} tokens into user data. This preg_replace_callback
version doesn’t cause problems if the replacement contains these tokens.