I have some SQL queries that use PHP string variables to create the query before PDO prepare().
$connection = new PDO(...);
// Make variable placeholder for each column.
$params = array();
foreach ($row as $col => $value) {
$params[':' . $col] = $value;
}
$columns = implode(', ', array_keys($row));
$values = implode(', ', array_keys($params));
$query = "
INSERT INTO my_table ($columns)
VALUES ($values)
";
$statement = $connection->prepare($query);
$statement->execute($params);
Or something similar with SELECT:
$query = "
SELECT field
FROM my_table
WHERE id IN ($ids)
";
Where the query will become
$query = "
SELECT field
FROM my_table
WHERE id IN (:id0, :id1, :id2)
";
and then the execute() function will pass in the params like array(':id0' => 0, ...).
Is this vulnerable to injection if the part being inserted is just a bunch of placeholders to be used for query preparation? And is there a better way to do this in PHP with PDO?
When binding a dynamic number of parameters, I revert to the ? placeholders. You can do:
$placeholders = implode(',', array_fill(0, count($values), '?'));
$query = "SELECT field FROM my_table WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($query);
$stmt->execute($values);
You still need to do string substitution if the column names are dynamic, as in your INSERT example. Those should be white-listed to prevent injection. But you can use the above mechanism for the values being inserted. You'll need to use
$values = array_values($params);
because ? placeholders can't be filled in from an associative array.
Related
I am trying to generate a single insert statement that will insert multiple rows. I have and array of values, which is what I am wanting to insert into a table that all use the same userkey.
I have tried using a named PDO parameter and binding to that, then passing in the role array during execute but that doesn't work. So I moved on to placeholders, but I can't get that to work either.
I call my function like addUsersRoles(1, [100,101,102]);
And looking at the generated SQL I get:
INSERT user_roles (userkey, roleid) VALUES (?,?),(?,?),(?,?)
Which I think is the correct format for inserting multiple records.
Based up on that, what I am trying to generate is:
INSERT user_roles (userkey, roleid) VALUES (1,100),(1,101),(1,102)
How can I combine the power of PDO's binding to a SQL statement in this manner?
public function addUsersRoles($userkey, $roles = []){
$in = str_repeat('?,', count($roles) - 1) . '?';
$base_user_sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$sql = $base_user_sql;
foreach ($roles as $role) {
//$sql .= "(:USERKEY, $in),"; // Didn't Work
$sql .= "($in),";
}
//Remove trailing comma
$sql = rtrim($sql, ',');
$db = static::getDB();
$stmt = $db->prepare($sql);
//$stmt->bindValue(':USERKEY', $userkey, PDO::PARAM_STR);
return $stmt->execute($roles);
}
You can use placeholders as well. Look at the following example:
public function addUsersRoles(string $userKey, array $roles = []): bool
{
$values = [];
$inputParameters = [':user_key' => $userKey];
foreach ($roles as $index => $role) {
$rolePlaceholder = ':roleid' . $index;
$values[] = sprintf('(:user_key, %s)', $rolePlaceholder);
$inputParameters[$rolePlaceholder] = $role;
}
$sql = 'INSERT INTO user_roles (user_key, roleid) VALUES ';
$sql .= implode(', ', $values);
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($inputParameters);
}
This code will generate a query like this:
INSERT INTO user_roles (user_key, roleid) VALUES (:user_key, :roleid0), (:user_key, :roleid1), (:user_key, :roleid2), (:user_key, :roleid3), (:user_key, :roleid4);
And the $inputParameters will be like this:
[
':user_key' => 'some user key',
':roleid0' => 1,
':roleid1' => 2,
]
You shouldn't use count($roles) when making $in. It's always just ?, ?. You just need the count of roles when repeating that for all the rows. You can use array_fill to create an array of (?, ?) strings, and then implode to put commas between them.
You also need to insert create an array with alternating keys and roles, and use that as the parameters when executing.
public function addUsersRoles($userkey, $roles = []){
$values = implode(',', array_fill(0, count($roles), '(?, ?)'));
$base_user_sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$sql = $base_user_sql . $values;
$keys_and_roles = [];
foreach ($roles as $role) {
$keys_and_roles[] = $userkey;
$keys_and_roles[] = $role;
}
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($keys_and_roles);
}
I've simplified it a bit, but this splits into two parts that are done together. The first is to generate the SQL...
INSERT user_roles (userkey, roleid) VALUES (?,?),(?,?),(?,?)
This code just loops through the roles and adds (?,?), for each one.
The second part is building up the bind data. As the SQL needs a list of the data in the order userkey, roleid pairs, as it's building the SQL, it also adds these values to a data array at the same time.
So the main code comes out as...
public function addUsersRoles($userkey, $roles = []){
$sql = 'INSERT user_roles (userkey, roleid) VALUES ';
$binds = [];
foreach ( $roles as $role ) {
$sql .= "(?,?),";
$binds[] = $userkey;
$binds[] = $role;
}
//Remove trailing comma
$sql = rtrim($sql, ',');
$db = static::getDB();
$stmt = $db->prepare($sql);
return $stmt->execute($binds)
}
(Although I haven't been able to test the execute part).
You should also make sure any errors are being dealt with as well.
I have an array that I am preparing for a SQL query, so following the steps to make it secure as possible. Attempting the following:
First I implode the array - I want the string result to come out as 'string1','string2','string3' and so on:
$in = "'" . implode("','", array_fill(0, count($finalArray), '?')) . "'";
I make my query:
$query = <<<SQL
UPDATE products SET `Status` = 'Reserved' WHERE `SerialNumber` in ($in);
SQL;
$query = <<<SQL
And prepare the statement variable:
$statement = $mysqli->prepare($query);
Then I attempt a bind_param with str_repeat, and this is where things go wrong:
$statement->bind_param(str_repeat('\'s\',', count($finalArray)), ...$finalArray);
I get:
mysqli_stmt::bind_param(): Number of elements in type definition string doesn't match number of bind variables
Does anyone know why I am getting this and how I can resolve it?
Looking at your dynamic creating of your placeholders:
$in = "'" . implode("','", array_fill(0, count($finalArray), '?')) . "'";
So seem to have creating them with ' quotations. Placeholders do not need quotations.
$in = implode(',', array_fill(0, count($finalArray), '?'));
$query = "UPDATE products SET Status = 'Reserved' WHERE SerialNumber IN ($in)";
$statement = $mysqli->prepare($query);
Then, in assigning types, you don't need them quoted also:
$statement->bind_param(str_repeat('s', count($finalArray)), $finalArray);
Sidenote: Take note that you'll also having to dynamically call bind_param thru call_user_func_array() since you're going to use an array. This part discusses it thoroughly.
Though I'd suggest/prefer to use PDO's ->execute():
$pdo = new PDO('mysql:host=localhost;dbname=DATABASE NAME', 'username', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$in = implode(',', array_fill(0, count($finalArray), '?'));
$query = "UPDATE products SET Status = 'Reserved' WHERE SerialNumber IN ($in)";
$statement = $pdo->prepare($query);
$statement->execute($finalArray);
Another way with using Reflection:
$in = implode(',', array_fill(0, count($finalArray), '?'));
$type = str_repeat('s', count($finalArray));
$query = "UPDATE products SET Status = 'Reserved' WHERE SerialNumber IN ($in)";
$statement = $mysqli->prepare($query);
$ref = new ReflectionClass('mysqli_stmt');
$method = $ref->getMethod('bind_param');
array_unshift($finalArray, $type); // prepend the 'sss' inside
$method->invokeArgs($statement, $finalArray);
$statement->execute();
The following script is used to implode values from a multidimensional array and insert into the mysql table. The table contains field data types of varchar and decimal. Since varchar type requires quotes and to avoid selectively placing quotes, I would like to put all values in quotes. How to implement it?
$values = array();
foreach ($data as $rowValues) {
foreach ($rowValues as $key => $rowValue) {
}
$values[] = "(" . implode(', ', $rowValues) . ",'".$date."')";
}
$query = "INSERT INTO mem (memno,loan,subsc,intst, date)
VALUES " . implode (', ', $values);
$result=mysql_query($query) or die();
I want the sql like this
INSERT INTO mem (memno,loan,subsc,intst, date)
values('value1', 'value2', 'valu3','value4','value5')
Don't use user input to build SQL strings - thats how you get SQL injection attacks.
Instead use a prepared statement:
$mysqli = new mysqli('localhost', 'my_user', 'my_password', 'world');
$stmt = $mysqli->prepare("
INSERT INTO mem( memno, loan, subsc, intst, date )
VALUES (?, ?, ?, ?, ?);
");
$stmt->bind_param('sssss', $memno, $loan, $subsc, intst, $date);
edit in response to the comment:
Dynamically binding an array of columns is pretty easy with PDO.
$db =new PDO("mysql:host=localhost;dbname=database;","root","");
/**
* #param PDO $db
* #param string $table - the table to insert into
* #param array $columns - which columns do we want to insert into
* #param array $data - a key/value array of the data we want to insert
* #return bool
*/
function insert_into($db, $table, array $columns, array $data) {
$rows = implode(', ', $fields);
$placeholders = array_map ( function($key){ return ":$key" }, $fields);
$placeholders = implode(', ', $fields);
$sql = "
INSERT INTO $table ($fields)
VALUES ($placeholders);
";
$stmt = $db->prepare($sql);
foreach( $fields as $field) {
$stmt->bindParam(":$field", $data[$field]);
}
return $sth->execute();
}
$inserted = insertInto(
$db
'mem',
array("memno", "loan", "subsc", "intst", "date"),
$data
);
Notice that the columns to insert are defined separately.
If I had used:
array_keys($data);
It would lead to a mass assigment vulnerability if $data comes from user input and is not whitelisted.
You can accomplish the same thing with mysqli but its a bit trickier.
If $rowValues array is as below then you can do like this also.
$rowValues = array(
"memno"=>"a",
"loan"=>"b",
"subsc"=>"c",
"intst"=>"d"
);
$fldStr = array();
$valStr = array();
foreach($rowValues as $key=>$val) {
array_push($fldStr, $key);
$v2 = "'" . $val . "'";
array_push($valStr, $v2);
}
array_push($fldStr, "date");
array_push($valStr, "'" . $date . "'");
$flds = implode(", ", $fldStr);
$vals = implode(", ", $valStr);
$query = "INSERT INTO mem ($flds) values($vals)";
I'm in the process of procedurally making a PDO statement, so far i've got
$sql = "UPDATE users SET ";
$values_array = array();
foreach($non_empty_fields as $key => $value){
$sql .= $key;
$sql .= " = :".$key.", ";
$values_array[':'.$key] = $value;
}
$sql = substr($sql, 0, -2);
$sql .= " WHERE id = :id";
$values_array[':id'] = $user_id;
$sth = $this->conn->prepare($sql);
print_r($non_empty_fields);
print_r($values_array);
echo($sql);
$sth -> execute($values_array);
$num_affected_rows = $sth -> affected_rows;
$sth -> close();
and when i run it, i get
Array
(
[gender] => female
[device_id] => 1213423489ydasxas98y76
)
Array
(
[:gender] => female
[:device_id] => 1213423489ydasxas98y76
[:id] => 35
)
UPDATE users SET gender = :gender, device_id = :device_id WHERE id = :id<br />
<b>Fatal error</b>: Call to a member function execute() on a non-object in <b>/Sites/api/include/DbHandler.php</b> on line <b>280</b><br />
I suspect its because I'm not setting the prepared statement properly, so my question is: how do you make a prepared statement from a string?
UPDATE:
call_user_func so the code now looks like:
$sql = "UPDATE users SET ";
$values_array = array();
foreach($non_empty_fields as $key => $value){
$sql .= $key;
$sql .= " = ?, ";
$values_array[] = &$value;
}
$sql = substr($sql, 0, -2);
$sql .= " WHERE id = ?";
$values_array[] = &$user_id;
$sth = $this->conn->prepare($sql);
$params = array_merge(array(str_repeat('s', count($values_array))), array_values($values_array));
call_user_func_array(array(&$sth, 'bind_param'), $params);
$sth -> execute();
now the error I'm getting isn't represented, pdo just doesn't update the table.
Oh Mysqli? i thought you said it was PDO. Mysqli doesnt support named markers like :id, you have to use question marks like ?
This parameter can include one or more parameter markers in the SQL statement by embedding question mark (?) characters at the appropriate positions.
Reference
And for PDO you can use either
Prepares an SQL statement to be executed by the
PDOStatement::execute() method. The SQL statement can contain zero or
more named (:name) or question mark (?) parameter markers for which
real values will be substituted when the statement is executed. You
cannot use both named and question mark parameter markers within the
same SQL statement; pick one or the other parameter style. Use these
parameters to bind any user-input, do not include the user-input
directly in the query.
Manual
I have a form field that is returning a comma-delimited string that I want to pass in to a PHP PDO MySQL query IN operation, but the IN operation requires that the values be comma-delimited (as opposed to my string of delimited values).
How do I do this?
$values = $_POST['values']; # '10,5,4,3' (string)
$query = "SELECT * FROM table WHERE id IN (:values)";
$data = array( ':values' => $values );
You can't pass in multiple values in a single placeholder. You will have to enter a different placeholder for each value to be passed into IN (). Since you don't know how many there will be, use ? instead of named parameters.
$values = explode(',', $values) ;
$placeholders = rtrim(str_repeat('?, ', count($values)), ', ') ;
$query = "SELECT * FROM table WHERE id IN ($placeholders)";
$stm = $db->prepare($query) ;
$stm->execute($values) ;
PDO alone cannot bind arrays to a :parameter. You need a helper function for that.
Also in your example, the literal string '10,5,4,3' would be received as one value. Parameter binding will turn it into .. id IN ('10,5,4,3') and not into a list comparison.
The workaround in your case would be to fall back on using dynamic SQL and escaping.
$values = preg_replace('/[^\d,]/', "", $_POST['values']);
$query = "SELECT * FROM table WHERE id IN ($values)";
I'm personally using a wrapper/helper function which has a special syntax for arrays (but you don't actually have one to begin with, so it would be a double workaround):
db("SELECT * FROM table WHERE id IN (??)", explode(",",$values));
The trick is to recognize that $values is a bunch of individual values, and set up your query with this in mind. This is easier to do if you use ? placeholders instead of named placeholders. For example, you could do something like the following:
$values = explode(',', $_POST['values']); //array(10,5,4,3)
$placeholder_string = implode(',', array_fill(0, count($values), '?')); // string '?,?,?,?'
$query = "SELECT * FROM table WHERE id IN ($placeholder_string)";
$statement = $db->prepare($query);
$statement->execute($values);