Sanitise query in SQL and PHP using in_array() - php

Using MySQL and PHP, a typical (PDO) query looks like this:
// prepare the query
$q = $DB->prepare("SELECT * FROM table_name WHERE property = :value");
// run the query
$q->execute(array(':value'=>$value));
This is safe from SQL injection, as the property value is treated separately to the query.
However if I want to use the same code to write a query that might retrieve a different field or different grouping, you cannot use the prepare/execute method alone, as you cannot use PDO parameters for fields (see here).
Can you simply use in_array() to check a field name, like this:
// return false if the field is not recognised
if(! in_array($field_name, array('field1','field2','field3')) return false
// run the query
$q = $DB->query("SELECT * FROM table_name ORDER BY " . $field_name);
Is there a safer / quicker way?

Already seems quite fast and secure. Maybe add backticks around the field name in the query.
To speed it up slightly you can use an assoc array and just check if the index exists instead of searching the contents of an array.
$fields = array('field1' => null, 'field2' => null, 'field3' => null);
if (!array_key_exists($field_name, $fields)) return false;
furthermore isset is faster than array_key_exists
if (!isset($fields[$field_name])) return false;
function benchmarks

Related

MySQL insert not showing anything

How can I make the database entry code work? Right now nothing shows when I run this code.
public function hookActionValidateOrder()
{
$products=[];
$ids_product_attribute=[];
$references=[];
$stocks= [];
for($i=0;Context::getContext()->cart->getProducts()[$i]['id_product'];$i++)
{
array_push($products,Context::getContext()->cart->getProducts()[$i]['id_product']);
array_push($ids_product_attribute,Context::getContext()->cart->getProducts()[$i]['id_product_attribute']);
array_push($references,Context::getContext()->cart->getProducts()[$i]['reference']);
array_push($stocks,Context::getContext()->cart->getProducts()[$i]['stock_quantity']);
die(Db::getInstance()->execute("
INSERT INTO cart_log (products, ids_product_attribute, references, stocks, time)
VALUES ($products[$i], $ids_product_attribute[$i], $references[$i], $stocks[$i])"));
}
// var_dump($products);
// var_dump($ids_product_attribute);
// var_dump($references);
// var_dump($products);
}
I would write this function this way:
public function hookActionValidateOrder()
{
$defaults = [
'id_product' => null,
'id_product_attribute' => null,
'reference' => 0,
'stock_quantity' => 0
];
$stmt = Db::getInstance()->prepare("
INSERT INTO cart_log
SET products = :id_product,
ids_product_attribute = :id_product_attribute,
references = :reference,
stocks = :stock_quantity");
$products = Context::getContext()->cart->getProducts();
foreach ($products as $product) {
$values = array_merge($defaults, array_intersect_key($product, $defaults));
$stmt->execute($values);
}
}
This example shows usage of query parameters in a prepared statement, so you don't try to interpolate PHP variables directly into your SQL query. Query parameters make it easier to write error-free SQL code, and they protect you from accidental SQL injection errors (also malicious attacks).
I recommend calling your cart->getProducts() once, and save the result in a local variable. I'm not sure what that function does, but I suppose it's running another SQL query. You shouldn't run the same SQL query many times for each loop, that will increase load to your database server.
The business about array_merge(array_intersect_key()) is to make sure the values array has both all of the needed keys, and no other keys besides the needed keys. Then it can be passed as-is to PDOStatement::execute().
I'm using an alternative form for INSERT that is supported by MySQL, using SET column = value ... syntax. I find this makes it easier to make sure I've matched a column to each value and vice-versa.
As mentioned in the comments above, you must enable exceptions in the database connector. I'm assuming your getInstance() function returns a PDO connection, so you can pass the array of parameters to execute(). You need to enable PDO exceptions as described here: https://www.php.net/manual/en/pdo.error-handling.php
If you don't enable exceptions, you should check the return value of prepare() and execute() to see if either is === false, and if so then log the errorInfo() (read the PHP doc I linked to about error handling).
I detected two error in your query:
You insert 5 fields, but only 4 values.
In value you don't add '' char if values string
Example: if all fields with data type string
VALUES ('$products[$i]', '$ids_product_attribute[$i]', '$references[$i]', '$stocks[$i]', 'Field 5')

Dynamic query creation MySQL

I'm working on a project using mysqli library, and I have reached the point where I need to create a SELECT query inside a method, depending on the parameters that were sent.
The behavior I'm looking for is similar to Android's SQLite where you pass the columns as a parameter and the values as the next paramenter.
I know I could create the query string if the parameter sent where the columns and the values, by iterating over them and then manually concatenating strings to a final query string, but I wonder if there is any core library that let you do this or any other way
You should use PDO prepared statements
//$param = array(":querycon1" => "querycon", ":querycon2" => "querycon2");
// $condition = "abc=:querycon1 AND xyz=:querycon2";
protected function setQuery($column,$condition,$param){
$this->query = "SELECT $column FROM tablename WHERE $condition";
$this->param = $param //
$this->getQuery($this->query, $this->param); // to a method that processes the query
}

Dynamic query with PDO and FIND_IN_SET

I'm using PDO to build my queries which rely on FIND_IN_SET to select certain rows. The elements of the set are passed via POST to the server and the query constructed like this:
SELECT * FROM table WHERE
(FIND_IN_SET('param1', column) > 0 OR FIND_IN_SET('param2', column) > 0)
That's, in a basic way, how the queries are constructed. Because the number of parameters to find in the set is dynamic, the FIND_IN_SET part of the query is made dynamically:
$q = implode("', column)>0 OR FIND_IN_SET('", $array);
And then applied to the query, which looks like this:
SELECT * FROM table WHERE
AND (FIND_IN_SET(:q, e.event_type)>0);
And the query eventually executed as:
$countResult->execute(array(':q' => $q);
Note that $q may take the form of:
"param1', column)>0 OR FIND_IN_SET('param2', column)>0 OR FIND_IN_SET('param3"
If I don't use PDO, the query executes correctly, but when using PDO, the query works when there is only one element in the array, but doesn't return any results when the array contains more than one element.
Is the execute method doing something to the parameter possibly?
You can use array_map() to create a FIND_IN_SET() expression from each array element, then implode() the results with ' OR ' as glue:
function fis($c) {
return function($k) {return "FIND_IN_SET(:$k, $c)";}
}
$arr = array(
'p1' => 'param1',
'p2' => 'param2'
);
$qry = $dbh->prepare('
SELECT *
FROM table
WHERE ('.implode(' OR ', array_map(fis('column'), array_keys($arr))).')
');
$qry->execute($arr);

How can I make this query injection-proof? (PHP)

The Gist
I want to perform an SQL query that depends on a variable number of parameters in my GET without being vulnerable to SQL injection.
The Parameters
My URL can be formed like this:
https://www.example.com/index.php?param1=blah1,param2=blah2,param3=a,b,c
or like this:
https://www.example.com/index.php?param1=blah1,param2=blah2,param3=a,b,c,d,e,f,g
In other words, param3 can have a variable number of comma-delimited parameters a,b,c,etc.
The White-list
I check to make sure that all parameters in a,b,c,etc. are in an approved white-list before I perform the query.
// $valid_params is an array of pre-approved parameters.
$arr = explode(',', clean($_GET['param3']));
$params = Array();
foreach($arr as $param){
if(in_array($param, $valid_params)){
array_push($params, $param);
}
}
The Query
I set up my database connection like this (with MySQL):
$db_connection = new PDO("mysql:host={$DB_HOST};dbname={$DB_NAME}",$DB_USER,$DB_PASS);
$db_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$db_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
And I want to perform a query like this (except safely):
$comma_separated_params = implode(',',$params);
$result = $db_connection->query("SELECT {$comma_separated_params} FROM some_table");
The Goal
Does anyone know how I could do this safely and efficiently?
Depending on your concern for overhead, you could just SELECT * and then filter the array in PHP - if the parameter is never sent to the database then there is no room for injection.
However it's not exactly the most elegant solution. Here's how I'd do it:
$comma_separated_params =
implode(
",",
array_map(
function($a) {return "`".$a."`";},
array_intersect(
explode(",",$_GET['param3']),
$valid_params
)
)
)
);
That one-line-wonder (with newlines added for clarity) will take the $_GET['param3'] variable, split it on commas, intersect it with your valid parameters (instead of your foreach loop), wraps each element in backticks (see note below) and finally glues them together with commas.
See, backticks allow you to use literally any string as a field name. Usually it's to allow keywords as names, but it can also allow for column names with spaces, and so on. The only character that has meaning within the backticks are backslashes and backticks - which it is safe to assume are not present since they'd have to be in your list of $valid_params to get this far.
Whitelisting is the way to go here. If you only allow things in that you've already specifically defined you should be okay. As for how efficient, this is all relative. The version you're using will perform well for relatively small lists, such as those with under 100 columns, so I wouldn't worry.
Bonus points for using PDO.
There's a chance your definition of 'allowed' columns and what's actually in the database might diverge. A more relaxed specification might be to grab the fields using SHOW FIELDS for the table in question and only allow those.
If you are only allowing a specific list of predefined values to be passed in param 3, and you are comparing the input values against them, I don;t think you have any injection exposure, as you then have full control over the value that ultimately go into your $comma_seperated_params variable.
This needs some work to finish, but with parameter binding it would look like this:
$binding = array();
$selects = array();
foreach ( $params as $value ) {
$binding[] = ':' . $value;
$selects = '?';
}
$select = implode(',', $select);
$result = $db_connection->prepare("SELECT $select FROM some_table");
foreach ( $binding as $key => $bind ) {
$result->bindParam($key, $bind, PDO::PARAM_STR);
}
$result->execute();
PDO::prepare will help you. This is exactly is recommended by experts. Never use mysql_real_escape_string (string). Always go for prepared statements.

SQL full text search with PHP and PDO

I'm trying to write a simple, full text search with PHP and PDO. I'm not quite sure what the best method is to search a DB via SQL and PDO. I found this this script, but it's old MySQL extension. I wrote this function witch should count the search matches, but the SQL is not working. The incoming search string look like this: 23+more+people
function checkSearchResult ($searchterm) {
//globals
global $lang; global $dbh_pdo; global $db_prefix;
$searchterm = trim($searchterm);
$searchterm = explode('+', $searchterm);
foreach ($searchterm as $value) {
$sql = "SELECT COUNT(*), MATCH (article_title_".$lang.", article_text_".$lang.") AGINST (':queryString') AS score FROM ".$db_prefix."_base WHERE MATCH (article_title_".$lang.", article_text_".$lang.") AGAINST ('+:queryString')";
$sth = $dbh_pdo->prepare($sql);
$sql_data = array('queryString' => $value);
$sth->execute($sql_data);
echo $sth->queryString;
$row = $sth->fetchColumn();
if ($row < 1) {
$sql = "SELECT * FROM article_title_".$lang." LIKE :queryString OR aricle_text_".$lang." LIKE :queryString";
$sth = $dbh_pdo->prepare($sql);
$sql_data = array('queryString' => $value);
$sth->execute($sql_data);
$row = $sth->fetchColumn();
}
}
//$row stays empty - no idea what is wrong
if ($row > 1) {
return true;
}
else {
return false;
}
}
When you prepare the $sql_data array, you need to prefix the parameter name with a colon:
array('queryString' => $value);
should be:
array(':queryString' => $value);
In your first SELECT, you have AGINST instead of AGAINST.
Your second SELECT appears to be missing a table name after FROM, and a WHERE clause. The LIKE parameters are also not correctly formatted. It should be something like:
sql = "SELECT * FROM ".$db_prefix."_base WHERE article_title_".$lang." LIKE '%:queryString%' OR aricle_text_".$lang." LIKE '%:queryString%'";
Update 1 >>
For both SELECT statements, you need unique identifiers for each parameter, and the LIKE wildcards should be placed in the value, not the statement. So your second statement should look like this:
sql = "SELECT * FROM ".$db_prefix."_base WHERE article_title_".$lang." LIKE :queryString OR aricle_text_".$lang." LIKE :queryString2";
Note queryString1 and queryString2, without quotes or % wildcards. You then need to update your array too:
$sql_data = array(':queryString1' => "%$value%", ':queryString2' => "%$value%");
See the Parameters section of PDOStatement->execute for details on using multiple parameters with the same value. Because of this, I tend to use question marks as placeholders, instead of named parameters. I find it simpler and neater, but it's a matter of choice. For example:
sql = "SELECT * FROM ".$db_prefix."_base WHERE article_title_".$lang." LIKE ? OR aricle_text_".$lang." LIKE ?";
$sql_data = array("%$value%", "%$value%");
<< End of Update 1
I'm not sure what the second SELECT is for, as I would have thought that if the first SELECT didn't find the query value, the second wouldn't find it either. But I've not done much with MySQL full text searches, so I might be missing something.
Anyway, you really need to check the SQL, and any errors, carefully. You can get error information by printing the results of PDOStatement->errorCode:
$sth->execute($sql_data);
$arr = $sth->errorInfo();
print_r($arr);
Update 2 >>
Another point worth mentioning: make sure that when you interpolate variables into your SQL statement, that you only use trusted data. That is, don't allow user supplied data to be used for table or column names. It's great that you are using prepared statements, but these only protect parameters, not SQL keywords, table names and column names. So:
"SELECT * FROM ".$db_prefix."_base"
...is using a variable as part of the table name. Make very sure that this variable contains trusted data. If it comes from user input, check it against a whitelist first.
<< End of Update 1
You should read the MySQL Full-Text Search Functions, and the String Comparison Functions. You need to learn how to construct basic SQL statements, or else writing even a simple search engine will prove extremely difficult.
There are plenty of PDO examples on the PHP site too. You could start with the documentation for PDOStatement->execute, which contains some examples of how to use the function.
If you have access to the MySQL CLI, or even PHPMyAdmin, you can try out your SQL without all the PHP confusing things. If you are going to be doing any database development work as part of your PHP application, you will find being able to test SQL independently of PHP a great help.

Categories