PDO::Prepare safety - php

I've read that the PDO::Prepare function creates a safe query. Does this mean escape characters don't need to be manually literalised? Such as the backslash character.

No it absolutely does not mean that. What you read is misleading.
There is a difference between a "prepared statement" and a "parameterized query." You want the latter for sanitation purposes.
For example:
$pdo->prepare("SELECT * FROM t1 WHERE col1 = $USER_PROVIDED_VALUE");
is not safe at all even though it is prepared. Instead, you have to do this:
$stmt = $pdo->prepare("SELECT * FROM t1 WHERE col1 = ?");
$stmt->execute(array($USER_PROVIDED_VALUE));
Preparing the query isn't going to do anything for you in terms of security if you do not properly parameterize it.

Related

Understanding PDO Prepared Statements and Binding Parameters

From experience and also having been told constantly the benefits of using prepared statements and binding my parameters, I have constantly used those two techniques in my code, however I would like to understand exactly the purpose of each of those two techiques:
From my understanding of prepared statements:
$sql = "SELECT * FROM myTable WHERE id = ".$id;
$stmt = $conn->prepare($sql);
$stmt->execute();
The previous code should create a sort of a buffer in the database with the query I proposed. Now FROM MY UNDERSTANDING (and I could be very wrong), the previous code is insecure, because the string $sql could be anything depending on what $id actually is, and if $id = 1; DROP TABLE myTable;--, I would be inserting a malicious query even though I have a prepared statement.
FROM MY UNDERSTANDING this is where binding my parameters com in. If I do the following instead:
$sql = "SELECT * FROM myTable WHERE id = :id";
$stmt = $conn->prepare($sql);
$stmt->bindParam(':id', $id);
$stmt->execute();
The database should know exactly all the parts of the sql statement before hand:
SELECT these columns: * FROM myTable and WHERE id = "a variable that was input by the user", and if "a variable that was input by the user" != a variable, the query fails.
I have been told by some my understanding is correct, and by others that it is false, could someone please let me know if I am wrong, correct, or missing something? And elaborate as much as you want, all feedback is greatly appreciated!
You're correct that the first case is insecure. It's important to understand though, that preparing a statement only has value if you are using variable data, and/or executing the same query repeatedly. If you are executing plain statements with no variables, you could simply do this:
$sql = "SELECT * from myTable WHERE this_column IS NOT NULL";
$result = $conn->query($sql);
And end up with a PDOStatement object to work with, just like when you use PDO::exec().
For your second case, again, you're largely correct. What's happening is the variable passed to the database is escaped and quoted (unless you specify otherwise with the third argument to PDOStatement::bindParam(), it's sent as a string which is fine for most cases.) So, the query won't "fail" if bad data is sent. It behaves exactly as if you had passed a valid number that didn't exist as an ID in the database. There are, of course, some edge cases where you are still vulnerable even with a correctly prepared statement.
Also, to make life easier, you can use prepared statements like this, to do implicit binding:
$sql = "SELECT * FROM myTable WHERE id = :id";
$stmt = $conn->prepare($sql);
$stmt->execute([":id"=>$id]);
Or even like this, with un-named parameters:
$sql = "SELECT * FROM myTable WHERE id = ?";
$stmt = $conn->prepare($sql);
$stmt->execute([$id]);
Naturally, most of this has been explained in the comments while I was typing up the answer!

MySQLi prepared statements vs queries

It seems like every question on this topic is the difference between mysqli prepared statements and straight mysqli queries in PHP, but nothing on what to do when prepared statements don't meet your needs.
A prepared statement is certainly the way to go when performing a simple query:
$stmt = $connection->prepare("SELECT * FROM my_table WHERE id = ?");
But what about when things get more complicated? From the PHP manual:
However, [markers] are not allowed for identifiers (such as table or column names), in the select list that names the columns to be returned by a SELECT statement, or to specify both operands of a binary operator such as the = equal sign.
This becomes a problem with complicated queries that do need to specify both operands of a binary operator, or some of the other restrictions that mysqli_prepare has.
In my case, I need to perform some queries to return results for blog entries (This is a simplified example, my connection variable is actually a private property of a blog class, but you get the idea):
$query = $connection->query("SELECT * FROM my_table WHERE $field = '$search'");
In this example, the $field variable is what column to search by, and the $search variable is what to search for. This type of query is not possible with prepared statements.
I've done a lot of careful planning for functions such as these, and since I know that there are only X amount of columns to search by, I use conditionals to check that $field is equal to one of those columns, and mysqli_real_escape_string to escape any possible quote characters. But is this good practice? Based on what I've read and answers here on SO, you should always use prepared statements, but I have never seen complicated queries in those examples. Is there a better way to prevent SQL injection, a more advanced way to use prepared statements, or should I just stick to very careful validation here?
Yes and no: It is necessary to check the $field variable agains a white-list - that is the only way to prevent sql injection - but there is no point in using mysqli_real_escape_string on the $field variable. If your column name is a reserved word or starts for example with a number, you should quote it in backticks but that is it.
You should still use a prepared statement for the $search variable, although here mysqli_real_escape_string would also do (instead of a prepared statement, not both).
Regarding:
$query = $connection->query("SELECT * FROM my_table WHERE $field = '$search'");
This type of query is not possible with prepared statements.
It is possible to achieve this using prepared statements. As long as the input doesn't come from the client, it is (somewhat) safe to use concatenation on strings when you perform a query, for example:
'SELECT * FROM `my_table` WHERE `'.$field.'` = ?'
Note that I've included the ? marker for my prepared statement, and it won't break my query as long as $field is a valid column name.
However, it's a bad practice to do so, instead you should implement a switch or an if … else block if you want to perform your query on different column or table names. Example:
switch($columnId) {
case 0:
$q = 'SELECT * FROM `my_table` WHERE `column0` = ?';
break;
case 1:
$q = 'SELECT * FROM `my_table` WHERE `column1` = ?';
…
}
Update:
Or use a whitelist with your column names, as hakre suggests:
$columns = ['column0', 'column1', 'column2'];
$columnId = $_GET['column'];
if ( isset($columns[$columnId]) ) $field = $columnId;
else throw new Exception('Column not defined');
$q = 'SELECT * FROM `my_table` WHERE `'.$columns[$columnId].'` = ?';
Or
if ( isset($columns[$columnId]) ) $field = $columns[$columnId];
else throw new Exception('Column not defined');
$q = 'SELECT * FROM `my_table` WHERE `'.$field.'` = ?';
In this case I used a numeric index, you can use a key o whatever serves you better.
But if the input comes from the user you should always use prepared statements. It's either that or manually scape your strings.

Safety of PDO param bindings

I am using parametrized queries with PDO to retrieve data from a MySQL database with a fairly complicated query. As a part of doing this, I use some code similar to this:
The query:
$query = "SELECT * FROM table WHERE something = :id_$id";
$stmt = $db->prepare($query);
The param binding:
$stmt->bindParam(":id_$id", $id);
My understanding is that PDO sanitizes your parametrized input by 'cleaning' the replacement string in the call to bindParam, but my question is:
Could an attacker exploit a construct like the above (via the value of $id) in order to inject undesirable SQL?
PDO does a textual replacement on the :id_$id with the sanitized value of $id, so I would think that no part of :id_$id (no matter what it ends up being) should end up in the final query, but I would love to get a definite answer!
Edit: It looks like I wasn't as clear as I should have been in explaining why I believe this could be a safe thing to do. Of course, I don't suggest that this is a good way to do things.
The reason I think this could be safe is that PDO (correct me if this is wrong) does a textual replacement of the sanitized bound-param on the replacement text. Intuitively, this should indicate that the replacement text (":id_$id") can be any value, since it will be entirely replaced by PDO when the parameter is placed in the query. Since the parameter replacing involves sanitizing the value of the parameter, while ":id_$id" may be dangerous to execute, "$id" (which is what appears in the final query) should be safe.
That's my reasoning, anyways. I'm not doing anything this dangerous in my code, so this is more of an academic interest.
Of course it's vulnerable.
However, using named placeholders is entirely optional. So, you don't have to use them at all:
$query = "SELECT * FROM table WHERE something = ?";
$stmt = $db->prepare($query);
$stmt->execute(array($id));
And, you know, whatever fairly complicated code can be simplified.
pdo does sanitize the input, you can further specify types in the third argument of bindValue/bindParam. I'd avoid using $ as part of your token as it could be interpreted by php if in "":
$query = "SELECT * FROM table WHERE something = :id;";
$stmt = $db->prepare($query);
$stmt->bindParam(":id", $id, PDO::PARAM_INT);
this would ensure that if $id is not an int pdo should raise an exception.
Even attempting to inject sql in this manner is escaped.
You should consider using a closed set of allowed values for $id. I mean:
switch ($id) {
case "value01":
$param = ":id_01";
break;
case "value02":
$param = ":id_02";
break;
default:
// safe value
$param = ":id_00";
}
$query = "SELECT * FROM table WHERE something = $param";
$stmt = $db->prepare($query);
$stmt->bindParam("$param", $id);

PHP: using prepared statements and protecting against SQL injection vs escape

I do understand that the prepared statements is the ultimate way to seek protection against the SQL injection. However, they provide coverage in a limited fashion; for example, in cases where I let the user to decide how the order by operation to be ( i.e, is it ASC or DESC? etc ), I get no coverage there with the prepared statements.
I understand that I can map the user input to a pre-defined white list for that. But, this is only possible when a whitelist can be created or guessed thoroughly beforehand.
For example, in the cases I mention above ( the ASC, or DESC ), this can easily be mapped and verified against a list of accepted values. But isn't there a situation where the portion of the SQL statement cannot be verified against a white list?
If such a situation exists, then what's the recommended approach?
If I were to escape the user_input using the underlying database's built-in escape utility (such as mysqL_real_escape_string for mysql) across the board, where would I fail?
I'm asking this question with the assumption that I always construct my sql statements with quoted values - even for integers...
Let's take a look at the following example and reflect upon it..
select {$fields} from {$table} where Age='{$age}' order by {$orderby_pref}
Assume all vars are user supplied.
If I were to mysql_real_escape_string all the variables in the above SQL ( as opposed to using prepared statements which covers me only half-way forcing me to come up whitelists for the other half that it cannot help), wouldn't it be equally safe (and easier to code)? If not, in which input scenario escape utility would fail?
$fields = mysql_escape($fields);
$table = mysql_escape($table);
$age = mysql_escape($age);
$orderby_pref = mysql_escape($orderby_pref);
select {$fields} from {$table} where Age='{$age}' order by {$orderby_pref}
You always need to use white-lists for stuff like table- or column names, whether you use prepared statements or the mysql escape functions.
The problem is that table names and column names are not quoted in single or double quotes, so if you use a function that specifically quotes these characters (and some more of course...), it will do nothing for your table name.
Consider the table name my_table; DELETE * FROM mysql; SELECT * FROM my_table. Nothing in this string will get escaped by mysql's escape functions but it is definitely a string you would want to check against a white-list.
Apart from that the mysql escape functions have a problem with character sets that can render them useless, so you are always better off with prepared statements.
You could use PDO and your life will get easier ... :
# Order
switch(strtoupper($Order)){
default:
case 'ASC':
$Order = 'ASC';
break;
case 'DESC':
$Order = 'DESC';
break;
}
# ID
$ID = 39;
$Username = 'David';
# Query
$Query = $this->DB->Main->prepare('SELECT * FROM Table WHERE ID = :ID AND Username = :Username ORDER BY HellBob '.$Order);
$Query->bindValue(':ID', $ID, PDO::PARAM_INT);
$Query->bindValue(':Username', $Username, PDO::PARAM_STR);
# All good ?
if(!$Query->execute()){
exit('Error');
}
// Results
$Row = $Query->fetch(PDO::FETCH_ASSOC);
You don't have to worry about quotes or SQL injections. You can use simple "white list" as you mention to get variable into your query.

How will this affect chances of preventing SQL injection?

I have posted about this before but never in this regard so please take a look:
I was told one way to do sql injections was to use 1=1 where someone can see all entries that don't belong to them.
But lets say i structure my query so that it also selects the user_id of the current user, would that work:
$userid = Current users stored id in database;
$postid = mysql_real_escape_string($_GET['id']);
And now lets assume that i enter: domain.com/page.php?id='' OR '1'='1'
Select article_name from table where user_id=$userid and post_id=$postid
Will the query still return everything or will it not since i have added the User_id barrier?
If you use PDO you don't have to worry about escaping data (in this situation):
$stmt = $pdo->prepare('SELECT article_name FROM table WHERE user_id = :userid AND post_id = :postid');
$stmt->execute(array(
':userid' => $userid,
':postid' => intval($_GET['id']) //Just to be safe
));
// You could also do this instead (thanks #Digital Precision)
//$stmt->bindValue(':postid', $_GET['id'], PDO::PARAM_INT);
//$stmt->execute(array(':userid' => $userid));
while($row = $stmt->fetch()) {
//Work with data
}
For more on PDO see the PHP docs.
The problem with using mysql_real_escape_string() is that as its name suggests it only escapes strings. It escapes the characters that can be used to terminate a string so that an attacker can't close a string and enter malicious SQL.
If you are stubborn and refuse to use PDO, you could use a function like intval() on any unsanitized integers to ensure they contain only numbers.
$post_id = intval($_GET['id']); //Now $post_id can only be a number
mysql_real_escape_string() is for sanitizing strings only. It does NOT protect from SQL injection in integers that are not wrapped in quotes, so your observation is correct: What is shown above is indeed not safe despite mysql_real_escape_string().
You need to either wrap your values in quotes:
Select article_name from table where user_id='$userid' and post_id='$postid'
or make sure that $userid and $postid are integers before running the query.
Not sure what you mean by "I was told one way to do sql injections was to use 1=1 where someone can see all entries that don't belong to them".
1=1 always evaluates to true. I've only ever seen this done when the query being generated by the application has only conditional where clauses with no root where clause. Not sure what it has to do with protecting you from sql injections.
Your query would look like:
Select article_name from table where user_id=$userid and post_id=\'\' OR \'1\'=\'1\'
As other mention while i typing this, it is better to quote your values. So you will have:
Select article_name from table where user_id=$userid and post_id='\'\' OR \'1\'=\'1\''
This returns nothing, if there is not a post with such id.
So your query will not return every post from the current user. But keep in mind to quote your values.

Categories