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.
Related
In my table i have query:
$sql="SELECT * FROM `jom_x1_organizatori`
WHERE Organizator='".$sta_dalje."' Order by `Struka`,`Zanimanje`";
$sta_dalje=$_POST["state_id"] from another table and value is:
ЈУ Гимназија са техничким школама Дервента
"ПРИМУС" Градишка
In case 1 working.
How to make query?
Firts of all: Never build the query by concatenating the query string with user input! If you do, then escape the input with the library's dedicated function (mysqli_real_escape_string for example). Without escaping you will open a potential security hole (called SQL injection).
"ПРИМУС" Градишка is not working because after concatenating, the query will be invalid. Now imagine, what happens, if I post the following input: '; DROP TABLE jom_x1_organizatori; --
Your query will be:
SELECT * FROM `jom_x1_organizatori`
WHERE Organizator=''; DROP TABLE jom_x1_organizatori; --' Order by `Struka`,`Zanimanje`
Whenever you can use prepared statements to bind parameters (and let the library to do the hard work), but always escape and validate your input (using prepared statements, escaping is done by the library)!
$sta_dalje = (sting)$_POST["state_id"]; // Do filtering, validating, force type convertation, etc!!
// Prepare the statement (note the question mark at the end: it represents the parameter)
$stmt = $mysqlLink->mysqli_prepare(
"SELECT * FROM `jom_x1_organizatori` WHERE Organizator = ?"
);
// Bind a string parameter to the first position
$stmt->bind_param("s", $sta_dalje);
For more info about prepared statements:
mysqli prepared statements
PDO prepared statements
Please note that the old mysql extension is deprecated, do not use it if not necessary!
Just a side note
Do not use SELECT * FROM, always list the columns. It prevents to query unnecessary data and your code will be more resistant to database changes, plus debugging will be a bit simplier task.
Use escape string
$sta_dalje = mysqli_real_escape_string($con, $_POST["state_id"]);
And your where condition can be simply
Organizator='$sta_dalje'
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.
Any way to prevent malicious sql statements without using prepared statements and parameterized queries?
Example after simplify:
<?php
$con = mysqli_connect($_POST['db_server'], $_POST['db_user'],
$_POST['db_password'], $_POST['db_database']) or die(mysql_error());
$result = mysqli_query($con, $_POST['query_message']);
?>
Is it possible to check out the parameter $_POST['query_message'] is safe or not?
You should always build your queries within your code and then sanitise any variables you're going to use within them. NEVER pass the query or the database connection variables in via $_POST unless your user is querying the database via that form, in which case I'd recommend you just install phpMyAdmin.
As for sanitising your variables, if you really don't want to use PDO's prepared statements, you can sanitise incoming integers as follows:
$id = (isset($_POST['id']) ? (int)$_POST['id'] : null);
if ($id) {
$sql = "SELECT *
FROM `table`
WHERE `id` = {$id}";
}
And for strings use this:
$username = (isset($_POST['username']) ? mysqli_real_escape_string($con, $_POST['username']) : null);
if ($username) {
$sql = "SELECT *
FROM `table`
WHERE `username` = {$username}";
}
You can also call real_escape_string() directly on your $con object as follows:
$username = (isset($_POST['username']) ? $con->real_escape_string($con, $_POST['username']) : null);
However, as with #Shankar-Damodaran above, I highly suggest you do use PDO prepared statements to query your database.
Why you don't wanna use Prepared Statements ? That is really weird. I strongly suggest you should go for it.
You could make use of mysqli::real_escape_string for escaping quotes that is commonly used for SQL Injection Attacks.
Something like...
OOP Style
$message = $mysqli->real_escape_string($_POST['query_message']);
Procedural Style
$message = mysqli_real_escape_string($link,$_POST['query_message']);
other way is using:
htmlentities($query);
as an extra you could use preg_match() regular expressions to avoid
the inclusion of certain words (SELECT, DROP, UNION .......)
Example:
try{
$query = sprintf("SELECT * FROM users WHERE id=%d", mysqli_real_escape_string($id));
$query = htmlentities($query);
mysqli_query($query);
}catch(Exception $e){
echo('Sorry, this is an exceptional case');
}
There are real world cases where prepared statements are not an option.
For a simple example, a web page page where you can do a search on any number of any columns in the database table. SAy that table has 20 searchable columns. you would need a huge case statement that has all 20 single column queries, all 19+18+17+16+15+14+13+... 2 column queries, all possible 3 column queries... that's a LOT of code. much less to dynamically construct the where clause. That's what the OP means by prepared statements being less flexible.
Simply put, there is no generic case. If there was, php would have it already.
real_escape_string can be beaten. a common trick is to % code the character you are trying to escape so real_escape_string doesn't see it. then it gets passed to mysql, and decoded there. So additional sanitizing is still required. and when all characters used in injection are valid data, it's a PITA, because you can't trust real_escape_string to do it.
If you are expecting an integer, it's super easy.
$sanitized=(int)$unsanitized;
done.
If you are expecting a small text string, simply truncating the string will do the trick. does't matter that it's not sanitized if there's not enough room to hold your exploit
But there is no one size fits all generic function that can sanitize arbitrary data against sql injection yet. If you write one, expect it to get put into php. :)
Where and when do you use the quote method in PDO? I'm asking this in the light of the fact that in PDO, all quoting is done by the PDO object therefore no user input should be escaped/quoted etc. This makes one wonder why worry about a quote method if it's not gonna get used in a prepared statement anyway?
When using Prepared Statements with PDO::prepare() and PDOStatement::execute(), you don't have any quoting to do : this will be done automatically.
But, sometimes, you will not (or cannot) use prepared statements, and will have to write full SQL queries and execute them with PDO::exec() ; in those cases, you will have to make sure strings are quoted properly -- this is when the PDO::quote() method is useful.
While this may not be the only use-case it's the only one I've needed quote for. You can only pass values using PDO_Stmt::execute, so for example this query wouldn't work:
SELECT * FROM tbl WHERE :field = :value
quote comes in so that you can do this:
// Example: filter by a specific column
$columns = array("name", "location");
$column = isset($columns[$_GET["col"]]) ? $columns[$_GET["col"]] : $defaultCol;
$stmt = $pdo->prepare("SELECT * FROM tbl WHERE " . $pdo->quote($column) . " = :value");
$stmt->execute(array(":value" => $value));
$stmt = $pdo->prepare("SELECT * FROM tbl ORDER BY " . $pdo->quote($column) . " ASC");
and still expect $column to be filtered safely in the query.
The PDO system does not have (as far as I can find) any mechanism to bind an array variable in PHP into a set in SQL. That's a limitation of SQL prepared statements as well... thus you are left with the task of stitching together your own function for this purpose. For example, you have this:
$a = array(123, 'xyz', 789);
You want to end up with this:
$sql = "SELECT * FROM mytable WHERE item IN (123, 'xyz', 789)";
Using PDO::prepare() does not work because there's no method to bind the array variable $a into the set. You end up needing a loop where you individually quote each item in the array, then glue them together. In which case PDO::quote() is probably better than nothing, at least you get the character set details right.
Would be excellent if PDO supported a cleaner way to handle this. Don't forget, the empty set in SQL is a disgusting special case... which means any function you build for this purpose becomes more complex than you want it to be. Something like PDO::PARAM_SET as an option on the binding, with the individual driver deciding how to handle the empty set. Of course, that's no longer compatible with SQL prepared statements.
Happy if someone knows a way to avoid this difficulty.
A bit late anwser, but one situation where its useful is if you get a load of data out of your table which you're going to put back in later.
for example, i have a function which gets a load of text out of a table and writes it to a file. that text might later be inserted into another table. the quote() method makes all the quotes safe.
it's real easy:
$safeTextToFile = $DBH->quote($textFromDataBase);
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.