I want to have a generic PHP function that builds a mysql query based on the parameters of the function. Since it is generic the query may sometimes ask for id=123 or name='Bob'. I test out some queries with quotes around numbers, even stuff like WHERE id > '50' + 7 and it worked but I have my doubts that this won't cause trouble down the road. I guess if this is really an all purpose function it should be able to handle dates and whatever other datatypes there are. So what would be the best way to form these queries safely?
Quotes around values are fine for any type as long as your query sticks to mySQL. The way the values will be treated will depend on the type of the field it's compared against. If necessary, they will be converted automatically.
As an aside, you may want to look into database wrappers that offer prepared statements like PDO. Apart from other advantages, they will take care of the quoting - and the escaping of incoming data - themselves.
An example from the manual:
<?php
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < :calories AND colour = :colour');
$sth->bindValue(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR);
$sth->execute();
?>
No quotes do no harm to any of data type. Because, mysql engine will convert it to corresponding columns' data type.
Empty quotes in mysql inserts act like they are interpreted as a string value.
If you are using a function for generating an insert for a mysql INT column and end up with
IntColumn = ''
you could get a type error.
ERROR 1366: 1366: Incorrect integer value: '' for column 'IntColumn' at row 1
You will want to filter out INT columns setting an empty value or put unquoted null instead of the empty quotes.
IntColumn = null
Related
This may be a little elementary for some, but in something like the following statement what would happen if the string was an integer (e.x 007 as in the movie):
$sth->bindParam(':colour', $colour, PDO::PARAM_STR, 12);
If colour was '007' would PDO::PARAM_STR, still work?
What is int 12 for? does it refer to the length of (colour & $colour)?
Is it's purpose to maximize the filter? (only strings of 12 get through?)
Thanks guys, still working on deciphering manual (new to PHP) but so far don't see specifics on this.
Complete statement here.
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < :calories AND colour = :colour');
$sth->bindParam(':calories', $calories, PDO::PARAM_INT);
$sth->bindParam(':colour', $colour, PDO::PARAM_STR, 12);
$sth->execute();
Most of time nothing wrong happens. As far as I know, SQL, just like PHP, is a loosely-typed language that allows you to represent a number as a string, so you can always format integers as strings. There are only few exceptional cases:
LIMIT clause in emulation mode or any other SQL clause that just cannot accept a string operand.
complex queries with non-trivial query plan that can be affected by a wrong operand type
peculiar column types, like BIGINT or BOOLEAN that require an operand of exact type to be bound (note that in order to bind a BIGINT value with PDO::PARAM_INT you need a mysqlnd-based installation).
Now to your example. 007 is actually a string, not integer. You can't get an integer with leading zeros in PHP. But in case there is a real integer, you still can bind it as a string.
What is int 12 for?
This number has a very specific purpose used with stored procedures only and doesn't have any effect on the regular queries.
So you can tell that the example is rather misleading.
I want to make a "dynamic" WHERE clause in my query based on a array of strings. And I want to run the created query using Mysqi's prepared statements.
My code so far, PHP:
$searchArray = explode(' ', $search);
$searchNumber = count($searchArray);
$searchStr = "tags.tag LIKE ? ";
for($i=1; $i<=$searchNumber-1 ;$i++){
$searchStr .= "OR tags.tag LIKE ? ";
}
My query:
SELECT tag FROM tags WHERE $searchStr;
More PHP:
$stmt -> bind_param(str_repeat('s', count($searchArray)));
Now this obviously gives me an error since the bind_param part only contains half the details it need.
How should I proceed?
Are there any other (better) way of doing this?
Is it secure?
Regarding the security part of the question, prepared statements with placeholders are as secure as the validation mechanism involved in filling these placeholders with values up. In the case of mysqli prepared statements, the documentation says:
The markers are legal only in certain places in SQL statements. For example, they are allowed in the VALUES() list of an INSERT statement (to specify column values for a row), or in a comparison with a column in a WHERE clause to specify a comparison value.
However, they 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. The latter restriction is necessary because it would be impossible to determine the parameter type. It's not allowed to compare marker with NULL by ? IS NULL too. In general, parameters are legal only in Data Manipulation Language (DML) statements, and not in Data Definition Language (DDL) statements.
This clearly excludes any possibility of modifying the general semantic of the query, which makes it much harder (but not impossible) to divert it from its original intent.
Regarding the dynamic part of your query, you could use str_repeat in the query condition building part, instead of doing a loop:
$searchStr = 'WHERE tags.tag LIKE ?' .
str_repeat($searchNumber - 1, ' OR tags.tag LIKE ?');
For the bind_param call, you should use call_user_func_array like so:
$bindArray[0] = str_repeat('s', $searchNumber);
array_walk($searchArray,function($k,&$v) use (&$bindArray) {$bindArray[] = &$v;});
call_user_func_array(array($stmt,'bind_param'), $bindArray);
Hopefully the above snippet should bind every value of the $bindArray with its corresponding placeholder in the query.
Addenum:
However, you should be wary of two things:
call_user_func_array expects an integer indexed array for its second parameter. I am not sure how it would behave with a dictionary.
mysqli_stmt_bind_param requires its parameters to be passed by reference.
For the first point, you only need to make sure that $bindArray uses integer indices, which is the case in the code above (or alternatively check that call_user_func_array doesn't choke on the array you're providing it).
For the second point, it will only be a problem if you intend to modify the data within $bindArray after calling bind_param (ie. through the call_user_func_array function), and before executing the query.
If you wish to do so - for instance by running the same query several times with different parameters' values in the same script, then you will have to use the same array ( $bindArray) for the following query execution, and update the array entries using the same keys. Copying another array over won't work, unless done by hand:
foreach($bindArray as $k => $v)
$bindArray[$k] = some_new_value();
or
foreach($bindArray as &$v)
$v = some_new_value();
The above would work because it would not break the references on the array entries that bind_param bound with the statement when it was called earlier. Likewise, the following should work because it does not change the references which have been set earlier up.
array_walk($bindArray, function($k,&$v){$v = some_new_value();});
A prepared statement needs to have a well-defined number of arguments; it can't have any element of dynamic functionality. That means you'll have to generate the specific statement that you need and prepare just that.
What you can do – in case your code actually gets called multiple times during the existence of the database connection - is make cache of those prepared statements, and index them by the number of arguments that you're taking. This would mean that the second time you call the function with three arguments, you already have the statement done. But as prepared statements don't survive the disconnect anyway, this really only makes sense if you do multiple queries in the same script run. (I'm deliberately leaving out persistent connections, because that opens up an entirely different can of worms.)
By the way, I'm not an MySQL expert, but would it not make a difference to not have the where conditions joined,but rather writing WHERE tags in (tag1, tag2, tag3, tag4)?
Solved it by the help of an answer found here.
$query = "SELECT * FROM tags WHERE tags.tag LIKE CONCAT('%',?,'%')" . str_repeat(" OR tags.tag LIKE CONCAT('%',?,'%')", $searchNumber - 1)
$stmt = $mysqli -> prepare($query);
$bind_names[] = str_repeat('s', $searchNumber);
for ($i = 0; $i < count($searchArray); $i++){
$bind_name = 'bind'.$i; //generate a name for variable bind1, bind2, bind3...
$$bind_name = $searchArray[$i]; //create a variable with this name and put value in it
$bind_names[] = & $$bind_name; //put a link to this variable in array
}
call_user_func_array(array($stmt, 'bind_param'), &$bind_names);
$stmt -> execute();
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);
The PDOStatement::bindValue() method offers a way to specify the type of the variable bound:
PDOStatement::bindValue ( $parameter , $value [, $data_type = PDO::PARAM_STR ] )
I'm wondering, what's the purpose of specifying the data type, whereas when leaved as default (PARAM_STR) eventually the database will anyway cast the value to the proper type before using it?
For example, if you have these queries over an INTEGER field:
INSERT INTO table (integerField) VALUES (?) ;
SELECT * FROM table WHERE integerField = ? ;
And you bind an integer in PHP, PDO will by default bind it as a string, which is equivalent as:
INSERT INTO table (integerField) VALUES ("1") ;
SELECT * FROM table WHERE integerField = "1" ;
That will work flawlessly, because the SQL database (at least MySQL, I'm not really aware of how that would work on other RDBMS) knows how to convert the string back to an integer before using it.
What are the use cases where it would make a difference to bound typed parameters vs strings?
I'm no PDO-expert, but I can think of a few scenarioes where the data_type parameter is both useful and even needed.
Output parameters
When you define output or input/output parameters, you must provide both type and length of the expected output parameter.
Ref: http://www.php.net/manual/en/pdo.prepared-statements.php
Example #4
$stmt = $dbh->prepare("CALL sp_returns_string(?)");
$stmt->bindParam(1, $return_value, PDO::PARAM_STR, 4000);
Example #5
$stmt = $dbh->prepare("CALL sp_takes_string_returns_string(?)");
$value = 'hello';
$stmt->bindParam(1, $value, PDO::PARAM_STR|PDO::PARAM_INPUT_OUTPUT, 4000);
DBMs without implicit casting
Explained in another answer to this question...
When parameter is not bound to castable data
Even databases with casting abilities will not always be able to cast you variable correctly.
Ref: Reasons to strongly type parameters in PDO?
$limit = 1;
$dbh->prepare("SELECT * FROM items LIMIT :limit");
$dbh->bindParam(":limit", $limit, PDO::PARAM_STR);
// Will throw "You have an error in your SQL syntax..."
That's mainly for interacting with databases that require correct typing. For example, if you enable strict mode in MySQL, you will get errors (failed queries) instead of warnings when there are type mismatches.
By default, MySQL does its best to convert data properly. But if you have ever seen 0000-00-00 in a date field, that is very likely the result of mysql trying to convert a string to a date and failing. In strict mode, the query would fail instead of trying to convert and using whatever the result is.
The data type parameter to PDOStatement::bindValue() isn't terribly useful. Essentially:
If you tell it PDO::PARAM_STR, it converts your value into a string.
If you tell it PDO::PARAM_INT and you pass a boolean, it converts it into a long.
If you tell it PDO::PARAM_BOOL and you pass it a long, it converts it into a boolean.
Nothing else seems to be converted. See here for a quick look at the source code and a little better explanation. Perhaps most importantly, PDO will not throw an exception or produce an error if you pass data with a type that doesn't match the data type you passed.
i am using php and running sql queries on a mysql server.
in order to prevent sql injections, i am using mysql_real_escape_string.
i am also using (int) for numbers casting, in the following manner:
$desired_age = 12;
$query = "select id from users where (age > ".(int)$desired_age.")";
$result = mysql_query($query);
that work.
But, when the variable contains larger numbers, casting them fails since they are larger than int.
$user_id = 5633847511239487;
$query = "select age from users where (id = ".(int)$user_id.")";
$result = mysql_query($query);
// this will not produce the desired result,
// since the user_id is actually being cast to int
Is there another way to cast large number (like BIGINT), except for the use of mysql_real_escape_string, when is comes to sql injection prevention?
If you are generating the user ID yourself there is no need to cast it for MySQL since there is no chance of SQL injection or other string issues.
If it is a user submitted value then use filter_var() (or is_numeric()) to verify it is a number and not a string.
You could use something like:
preg_replace('/[^0-9]/','',$user_id);
to replace all non numeric symbols in your string.
But there actually is no need to do so, simply use mysql_real_escape_string() as your integer value will be converted to a string anyway once $query is built.
Validate input. Don't just simply escape it, validate it, if it's a number. There're couple of PHP functions which do the trick, like is_numeric() - Finds whether a variable is a number or a numeric string
http://www.php.net/is_numeric
Use server-side prepared, parametrized statements (and thus remove the need for xyz_real_escape_string()) and/or treat the id as a string. The MySQL server has built-in rules for string<->number conversions and if you should decide to change to type/structure of the id field you don't have to change the php code as well. Unless you have concrete needs for (micro-)optimization there's usually no need to let the code make this kind of assumptions about the structure and value range of an id field in the database.
$pdo = new PDO('mysql:...');
$pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
$stmt = $pdo->prepare('SELECT age FROM users WHERE id=?');
$stmt->execute(array('5633847511239487'));
After some research I've come to such setup
private function escapeInt($value)
{
if (is_float($value))
{
return number_format($value, 0, '.', ''); // may lose precision on big numbers
}
elseif(preg_match('/^-?[0-9]+$/', $value))
{
return (string)$value;
}
else
{
$this->error("Invalid value");
}
}
Separate case for the floats because $i = 184467440737095; become float on a 32-bit system and thus will crumble to scientific notation when cast to string.
And simple regexp for the rest
You can even multiply the variable by *1, you can check it min and max values you can accept (for age bigint is not an option at all... so why even allow numbers more than values you are prepared for?
And there is also PDO with its query preparing.