I have a web application with lots of data, and a search/filter function with several fields, such as name, status, date, and so on. I have been using parameterized queries like this for the regular (non-search) queries:
$id = $_POST['itemID'];
$db = mysqli_connect($host, $username, $password, $database);
$sql_query = "SELECT * FROM tbl_data WHERE ID = ?";
$stmt_query = mysqli_prepare($db, $sql_query);
mysqli_stmt_bind_params($stmt_query, "i", $id);
mysqli_stmt_execute($stmt_query);
//and so on..
How would I protect agains SQL-injection with multiple, optional parameters? There can be up to 10 separate parameters that might or might not be set.
Edit as the question seems unclear:
My example was with one parameter, which is not optional. I know this protects against sql-injection. How would I go about doing this with 10 parameters, where one or several could be set at the same time? E.g. a query such as this:
SELECT * FROM tbl_data
WHERE NAME = ?
AND STATUS = ?
AND DATE = ?
AND PARAM4 = ?
AND PARAM5 = ?
where the user only wants to search on name and date. How would I do the binding? It's not a good idea to check for each of the 100 possible combinations of search terms.
You are already protected against sql injection, as you are using the mysqli_stmt_bind_params which will escape properly for you.
Edit.
You could switch to some database framework to have a clean and beautiful code.
Otherwise... this is so old spaghetti style... but I love it :D
It's quite easy to expand your code to work with an unknown number of parameters. You should just loop on your parameters and at the same time 1. build your query string with the question mark notation, and add your parameters to an array, which you will be passing to maxdb_stmt_bind_param ( resource $stmt , string $types , array &$var ).
So it would look like this. It assumes at least ONE parameter is there (but it's trivial to avoid this).
$sql = "SELECT * FROM tbl_data WHERE ";
$and = '';
$types = '';
$parameters = array();
foreach($_POST as $k => $v) {
// check that $k is on your whitelist, if not, skip to the next item
$sql .= "$and $k = ?";
$and = " AND ";
$parameters[] = $v;
$types .= 's';
}
$stmt_query = mysqli_prepare($db, $sql);
mysqli_stmt_bind_params($stmt_query, $types, $parameters);
I recommend switching to PDO. It's built into PHP like the mysqli extension, but has a cleaner syntax and allows you to pass in your parameter values as an array, which you can easily construct dynamically with as many elements as needed.
Related
This question already has answers here:
Use an array in a mysqli prepared statement: `WHERE .. IN(..)` query [duplicate]
(8 answers)
Closed 11 months ago.
I'm trying to create a select query with dynamic where clause and dynamic parameters but I always get error :
Warning: mysqli_stmt::bind_param(): Number of elements in type
definition string doesn't match number of bind variables
Which I sincerely do not understand since it seems the count is alright. So this is what the code really looks like in its rude format. I can't see what I'm doing wrong.
//get variables
$mediaArray ='Facebook,Twitter,Twitch,';
$otherMedia = 'House';
//convert string to array
$socialArray = explode(',', $mediaArray)
//declare some variables to be used later
$andwhere = '';
$bp = '';
$socialmarray = ''
//get every value from array of social media
foreach($socialArray as $socialmedia){
$socialmarray .=$socialmedia.',';
$andwhere .= " AND socialmedianame=?";
$bp .='s';
}
//test strings
echo $wheres = $andwhere;//AND socialmedianame=? AND socialmedianame=? AND socialmedianame=?
echo $bip = $bp.'s';//ssss
echo $validarayy = rtrim($socialmarray,',');//Facebook,Twitter,Twitch
//select query
$selectquery = $conn->prepare("select * from mediaservices where socialmedianame=? $wheres");
$selectquery->bind_param("$bip",$otherMedia,$validarayy);
$selectquery->execute();
$resultquery = $selectquery->get_result();
Because:
You are using user-supplied data, you must assume that your query is vulnerable to a malicious injection attack and
the amount of data that is to be built into the query is variable/indefinite and
you are only writing conditional checks on a single table column
You should use a prepared statement and merge all of the WHERE clause logic into a single IN statement.
Building this dynamic prepared statement is more convoluted (in terms of syntax) than using pdo, but it doesn't mean that you need to abandon mysqli simply because of this task.
$mediaArray ='Facebook,Twitter,Twitch,';
$otherMedia = 'House';
$media = array_unique(explode(',', $mediaArray . $otherMedia));
$count = count($media);
$conn = new mysqli("localhost", "root", "", "myDB");
$sql = "SELECT * FROM mediaservices";
if ($count) {
$stmt = $conn->prepare("$sql WHERE socialmedianame IN (" . implode(',', array_fill(0, $count, '?')) . ")");
$stmt->bind_param(str_repeat('s', $count), ...$media);
$stmt->execute();
$result = $stmt->get_result();
} else {
$result = $conn->query($sql);
}
foreach ($result as $row) {
// access values like $row['socialmedianame']
}
For anyone looking for similar dynamic querying techniques:
SELECT with dynamic number of LIKE conditions
INSERT dynamic number of rows with one execute() call
In your query:
$selectquery = $conn->prepare("select * from mediaservices where socialmedianame=? $wheres");
The ? represents one parameter to pass in, and the evaluation of $wheres adds another three, giving you four total parameters.
bind_param() should take a string representing the types of the variables to insert as the first parameter, and the variables themselves as the subsequent parameters.
In your bind:
$selectquery->bind_param("$bip",$otherMedia,$validarayy);
$bip evaluates to ssss and $otherMedia is a single string ("House"). You might expect $validarayy to be three strings, but rtrim() returns a string. Thus, it is only one string ("Facebook,Twitter,Twitch"). You pass through two variables when the query is expecting four:
$conn->prepare("select * from mediaservices where socialmedianame=House AND socialmedianame=Facebook,Twitter,Twitch AND socialmedianame=? AND socialmedianame=? AND socialmedianame=?"
To correct this, you'll want to convert $validarayy back to an array, and use the index for the various inputs:
$socialmarray2 = explode(',', $validarayy);
$selectquery->bind_param("$bip", $otherMedia, $socialmarray2[0], $socialmarray2[1], $socialmarray2[2]);
Also note that your sample code has a few missing semicolons; you'll need to fix these in order for your code to work correctly.
This can be seen working here.
Finally, note that even if you were to split the three strings out correctly, the selection of ... AND socialmedianame=Facebook AND socialmedianame=Twitter AND socialmedianame=Twitch will never match any results; socialmedianame can only contain one value. You're probably looking to substitute your AND statements with OR statements.
I'm working on a dynamic query that uses variables to specify a table, a field/column, and a value to search for. I've gotten the query to work as expected without the variables, both in phpMyAdmin (manually typing the query) and from within the code by concatenating the variables into a complete query.
However, when I use bindParam() or bindValue() to bind the variables, it returns an empty array.
Here's my code:
function search_db($db, $searchTerm, $searchBy, $searchTable){
try{
$stmt = $db->prepare('
SELECT
*
FROM
?
WHERE
? LIKE ?
');
$stmt->bindParam(1, $searchTable);
$stmt->bindParam(2, $searchBy);
$stmt->bindValue(3, '%'. $searchTerm.'%');
$stmt->execute();
} catch(Exception $e) {
return array();
}
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// database initialization, creates the $db variable
require(ROOT_PATH . "include/database.php");
$matches = search_db($db, 'search term', 'myColumn', 'myTable');
var_dump($matches);
Expected results: an array of rows from the database
Actual results: an empty array
Unfortunately, placeholder can represent a data literal only. So, a very common pitfall is a query like this:
$opt = "id";
$sql = "SELECT :option FROM t";
$stm = $pdo->prepare($sql);
$stm->execute([':option' => $opt]);
$data = $stm->fetchAll();
This statement will return just a literal string 'id' in the fieldset, not the value of the column named id.
So, a developer must take care of identifiers oneself - PDO offers no help for this matter.
To make a dynamical identifier safe, one has to follow 2 strict rules:
to format identifiers properly
to verify it against a hardcoded whitelist.
To format an identifier, one has to apply these 2 rules:
Enclose the identifier in backticks.
Escape backticks inside by doubling them.
After such formatting, it is safe to insert the $table variable into query. So, the code would be:
$field = "`".str_replace("`","``",$field)."`";
$sql = "SELECT * FROM t ORDER BY $field";
However, although such formatting would be enough for the cases like ORDER BY, for most other cases there is a possibility for a different sort of injection: letting a user choose a table or a field they can see, we may reveal some sensitive information, like a password or other personal data. So, it's always better to check dynamical identifiers against a list of allowed values. Here is a brief example:
$allowed = array("name","price","qty");
$key = array_search($_GET['field'], $allowed);
$field = $allowed[$key];
$query = "SELECT $field FROM t"; //value is safe
For keywords, the rules are same, but of course there is no formatting available - thus, only whitelisting is possible and ought to be used:
$dir = $_GET['dir'] == 'DESC' ? 'DESC' : 'ASC';
$sql = "SELECT * FROM t ORDER BY field $dir"; //value is safe
I was told that "There is no way to bind an array to an SQL statement using prepared statements" but I have done it. I am having trouble recreating it though.
I have a statement that updates the database:
if (isset($_POST['printRow'])){
$ids = "";
foreach ($_POST['checkbox'] as $rowid)
{
if(!empty($ids)) $ids .= ',';
$ids .= $rowid;
$_SESSION['ids'] = $ids;
}
}
Here I forgot to post the WORKING code:
$stmt = $conn->prepare("UPDATE just_ink SET deleted=1 WHERE ID IN( " . $ids . ")");
$stmt->execute();
But I still have the following problem:
Where $ids can be either one or multiple ids.
So here is the problem, if I try to take $ids and set a SESSION with it
($_SESSION['ids'] = $ids;)
For use on another page.
On the next page I want to select data using $_SESSION['ids'] so,
$stmt = $conn->prepare("SELECT * FROM just_ink WHERE ID IN( " . $_SESSION['ids'] . ")");
$stmt->execute();
But this doesn't work. Any ideas why?
It doesn't work, because, as you correctly said, you can't bind an array to an SQL statement using prepared statements.
The correct way to bind an array is to create a string of placeholders (question marks) and then bind params in a loop.
Let's say you have an array of necessary ID's called $checkboxes. First, we need to create a string that we will use in our query to bind required params. If $checkboxes has 3 items, our string will look like
$placeholder = "?,?,?";
For this we can use str_repeat function to create a string, where every but last element will add ?, part to placeholder. For last element we need to concatenate single question mark.
$placeholder = str_repeat('?,', count($checkboxes)-1).'?';
Now we need to form and prepare a query that will contain our placeholders:
$query = 'SELECT * FROM just_ink WHERE ID IN (".$placeholder.")';
$stmt = $conn->prepare($query);
To bind every ID to its placeholder we use bindParam method in a loop:
for ($i=0; $i<count($checkboxes); $i++) {
$stmt->bindParam($i+1, ($checkboxes[$i]); #position is 1-indexed
}
$stmt->execute();
You can use arrays with mysqli prepared statements by using call_user_func_array
Your code would end up something like this
$varArray = array();
$questionArray = array();
foreach ($_POST['checkbox'] as $daNumber=>$daValue) {
$questionArray[] = "?";
//We're declaring these as strings, if they were ints, they would be i
$varArray[0] .= 's';
//These must be relational variables. The ampersand is vry important.
$varArray[] = &$_POST['checkbox'][$daNumber];
}
//comma separated series of questionmarks
$allDaQuestions = implode(', ', $questionArray);
$query = "SELECT * FROM just_ink WHERE ID IN ($allDaQuestions)";
$stmt = $conn->prepare($query);
//Where the magic happens
call_user_func_array(array(&$stmt, 'bind_param'), $varArray);
//continue with your regularly scheduled broadcast
$stmt->execute();
//etc.
did you set session_start() at the beginning of the file? you can't use $_SESSION if you don't do that first
I have been old school using mysql_query and starting out now using PDO. Which is great!
But in my old scripts I had build a dynamic query builder, and i'm having a tough time porting that over using PDO.
If anyone can give me some direction that would be great!
Here is the theory of it.
I have an array of
the DB Fields and Values (upon insert).
Create the query string to product a valid PDO transaction
Here is a portion of what i'm trying to do.
public $dbFields; // This is an array of the fields plus VALUES
public function select($where, $limit) {
// This is what I **had** before
$query = "SELECT ". implode(", ", $this->dbFields) ." FROM ". $this->table." WHERE ". $where ." ". $limit."";
// Now i need to convert that to PDO
$this->connection->beginTransaction();
# START Query
$select = $this->connection->prepare("SELECT {$this->fieldNames} FROM {$this->table}");
// I need to BIND my params and values, but i'm not sure the best route to take when I have a WHERE clause that includes, "AND" / "OR" operators.
# EXECUTE the query
$select->execute();
$this->connection->commit();
}
This is what I HAD before
$results = $db->select("userId = 111 OR userId = 222");
But what i'm thinking I need to do is use something more like
$results = $db->select(array("userId"=>111, "userId"=>222));
I know this is a tall order, and I hope it makes sense in what i'm trying to do, but any help in trying to build these queries would be greatly appreciated.
You'll need a separate $params parameter to your select method. I took the liberty of providing defaults for the method parameters. Like #userXxxx notes, you don't need a transaction just to do a SELECT.
<?php
class db {
public $connection; //PDO
public $dbFields; // This is an array of the fields plus VALUES
public function select($where = '1', $params = array(), $limit = '', $fetchStyle = PDO::FETCH_ASSOC) { //fetchArgs, etc
$fields = implode(', ', $this->dbFields);
//create query
$query = "SELECT $fields FROM {$this->table} WHERE $where $limit";
//prepare statement
$stmt = $this->connection->query($query);
$stmt->execute($params);
return $stmt->fetchAll($fetchStyle);
}
//...
}
$where = 'userId IN(:userId1, :userId2)';
$params = array(':userId1' => 111, ':userId2' => 2222);
$db->select($where, $params);
Notes:
If you really want, you can add additional method parameters to match up with all the flexibility of PDOStatement::fetchAll.
I'm not sure what you mean about $dbFields being "fields plus VALUES". Can you explain?
[Edit]
You might want to take a look at the docs/examples for PDOStatement::execute, since that seemed to be where your confusion was rooted--in particular, the $input_parameters method parameter.
What about this?
public function select($where, $limit) {
$query = "SELECT ". implode(", ", $this->dbFields) ." FROM ". $this->table." WHERE ". $where ." ". $limit."";
$this->connection->query($query);
}
//Use what you had before:
$results = $db->select("userId = 111 OR userId = 222");
Not sure why you want to use transaction (for all-or-nothing basis or catching exceptions and rollback) or prepared queries (for sending multiple queries)...
I've been testing my wrapper for mysqli prepared statements, and have come accross something I didn't expect. In my table guests, first_name is a varchar[32] (basically a string), and id is an integer. My function uses getType on parameters like $first_name and $id in the example below to build the $types parameter for the bind_param method. I was ready to tackle issues like what happens when $id is taken from form input and is actually a string, or, what happens if somweone enters a string that is a number, when I realised it didn't matter 0_0
$SQL = 'update guests set first_name = ? where id = ?';
$mysqli->execute($SQL, $first_name, $id);
all the following cases resulted in successfull inserts:
$id = "1";
$first_name = "Frank";
$types param was 'ss';
$id = 1;
$first_name = 3;
$types param was 'ii';
So, what's the deal with this?
edit:
call_user_func_array(array($statement, 'bind_param'), $bind_params);
You think the way I'm calling bind_param is a factor? In any way, I would love to know why.
The $types parameter affects the generated SQL, so the first example gives the following SQL:
update guests set first_name = 'Frank' where id = '1';
and the second:
update guests set first_name = 3 where id = 1;
These are both valid SQL statements, as MySQL handles the type conversion for you. Hence, the $types parameter matters, but only up to the validity of the SQL statement it generates.