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')
Related
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
In our application, users can create custom export functions in form of SQL statements. Something like this:
SELECT name, age, date_birth FROM users WHERE group_id = 2
I don't want them to clear the whole database by inserting a DELETE statement.
My ideas would be:
Use an SQL account, which is only allowed to SELECT. (I don't want to do this, if there are alternatives.)
Use a magic regex, that checks whether the query is dangerous or not. (Would this be good? Is there already such a regex?)
We are using PHP PDO.
As I see it, there are three options to choose from:
Option 1
Create a tool that will create the query for the user on the background. Simply by clicking buttons and entering table names. This way you can catch all weird behavior in the background bringing you out of danger for queries you don't want executed.
Option 2
Create a MySQL user that is only allowed to do SELECT queries. I believe you can even decide what tables that user is allowed to select from. Use that user to execute the queries the user enters. Create a seperate user that has the permissions you want it to to do your UPDATE, INSERT and DELETE queries.
Option 3
Before the query is executed, make sure there is nothing harmfull in it. Scan the query for bad syntax.
Example:
// Check if SELECT is in the query
if (preg_match('/SELECT/', strtoupper($query)) != 0) {
// Array with forbidden query parts
$disAllow = array(
'INSERT',
'UPDATE',
'DELETE',
'RENAME',
'DROP',
'CREATE',
'TRUNCATE',
'ALTER',
'COMMIT',
'ROLLBACK',
'MERGE',
'CALL',
'EXPLAIN',
'LOCK',
'GRANT',
'REVOKE',
'SAVEPOINT',
'TRANSACTION',
'SET',
);
// Convert array to pipe-seperated string
// strings are appended and prepended with \b
$disAllow = implode('|',
array_map(function ($value) {
return '\b' . $value . '\b';
}
), $disAllow);
// Check if no other harmfull statements exist
if (preg_match('/('.$disAllow.')/gai', $query) == 0) {
// Execute query
}
}
Note: You could add some PHP code to filter out comments before doing this check
Conclusion
What you are looking to do is quite possible however you'll never have a 100 percent guarantee that it's safe. Instead of letting the users make the queries it's better to use an API to provide data to your users.
Don't do this, there will always be creative ways to make a dangerous query. Create an API that will manually construct your queries.
Since you said you would prefer not to use read only SQL accounts if there are alternatives. If you're running PHP 5.5.21+ or 5.6.5+: I'd suggest checking if the first statement of the query is a SELECT statement and disabling multiple queries in your PDO connection.
First disable multi statements on your PDO object...
$pdo = new PDO('mysql:host=hostname;dbname=database', 'user', 'password', [PDO::MYSQL_ATTR_MULTI_STATEMENTS => false]);
Then check that the first statement in the query uses SELECT and that there are no subqueries. The first regex ignores leading whitespace which is optional and the second detects parenthesis which would be used to create subqueries -- this does have the side effect of preventing users from using SQL functions but based on your example I don't think that's an issue.
if (preg_match('/^(\s+)?SELECT/i', $query) && preg_match('/[()]+/', $query) === 0) {
// run query
}
If you're running an older version, you can disable emulated prepares to prevent multiple statements being executed but this relies on PDO::prepare() being used.
FWIW: It would be a lot better to use prepared statements/generate safe queries for your users or to use a read-only SQL account. If you're using MySQL and have admin rights/remote access, I'd suggest using the community edition of SQLyog (https://github.com/webyog/sqlyog-community/wiki/Downloads) to create read-only user accounts. It's extremely user friendly so you won't have to learn the GRANT syntax.
For me,I'm prefer to use a MYSQL account only allowed SELECT.
If you want a regex,I think all SELECT SQLs are started with "select",any others? These are my codes:
$regex = "/^select/i";
if(preg_match($regex,$sql)){
//do your sql
}
I wanted a safer, more robust solution that didn't involve fully tokenizing query. Based on my experience writing SQL parsers (here, and here), I can say this solution is pretty bulletproof without having to use a full-featured query parser.
This is the best answer because:
It does not require a new SQL user with limited permissions
It does not require a new DB connection with limited permissions
It does not break if the query contains a string or a comment with the word "delete"
It allows complex queries with nested queries
It allows the user to input arbitrary SQL
It's safe
All the other answers as of the time of writing this have one or more of these limitations.
Here's how it works
Remove all inline and multi-line comments
Remove all single and double quoted strings
Remove all symbols and numbers
Create a unique array of the remaining (key)words
If any of the remaining keywords are INSERT, UPDATE, DELETE, RENAME, DROP, CREATE, TRUNCATE, ALTER, COMMIT, ROLLBACK, MERGE, CALL, EXPLAIN, LOCK, GRANT, REVOKE, SAVEPOINT, TRANSACTION, or SET then it is a "dangerous" query and should not be run.
Note:
This function does not validate the SQL, it only ensures that the SQL will not alter your database in any way. You will still need to run the query in a try/catch to make sure the query is valid.
/**
* Determine if an SQL statement could potentially alter the database in any way.
* #param string $sql - An SQL statement
* #return boolean - True if query could alter the database, else false
*/
function isDangerousQuery($sql){
$sql = trim($sql);
// Irrelevant tokens to be parsed out of the query
// A comment or string may contain a word like "drop"
// so comments and strings need to be removed from the query
$token_types = [
[ 'name' => 'Single-Line Comment',
'start' => "--",
'end' => "\n" ],
[ 'name' => 'Multi-Line Comment',
'start' => "/*",
'end' => "*/" ],
[ 'name' => 'Double-quoted String',
'start' => "\"",
'end' => "\"" ],
[ 'name' => 'Single-quoted String',
'start' => "'",
'end' => "'" ]
];
// This array will contain every character that is not part
// of one of the above described irrelevant tokens
$keywords_buffer = [];
// If we are currently parsing one of the above token types
// it's index is held here, else this will be false
$current_token_type_index = false;
// Loop through each character and reconstruct the query without the
// irrelevant token types. We need to loop rather than use a regex
// because there could be quotes nested in comments and things like that
// that would "trick" our regex
$length = strlen($sql);
for ($index = 0; $index < $length; $index++) {
$chunk = substr($sql, $index);
// If the current char is an escape char, skip the next char
if($sql[$index] === '\\'){
$index++;
continue;
}
// Looking for all starting tokens
if(false === $current_token_type_index){
foreach($token_types as $token_type_index => $token_type){
if(0 === strpos($chunk, $token_type['start'])){
$current_token_type_index = $token_type_index;
}
}
if(false === $current_token_type_index){
$keywords_buffer[] = $sql[$index];
}
// Looking for ending token
}else if(0 === strpos($chunk, $token_types[$current_token_type_index]['end'])){
$index += strlen($token_types[$current_token_type_index]['end']);
if(strpos($token_types[$current_token_type_index]['end'], "\n") !== false) $keywords_buffer[] = "\n";
$current_token_type_index = false;
}
}
// Reconstruct the sql without the irrelevant tokens
$sql_cleaned = implode('', $keywords_buffer);
// Remove all symbols from the sql leaving only keywords and numbers
$sql_keywords_only = preg_replace("/[^a-zA-Z_0-9\s]/", ' ', $sql_cleaned);
// Create an array of unique keywords in upper-case
$sql_keywords = array_unique(preg_split("/\s+/", strtoupper($sql_keywords_only)));
// Filter out numbers and empty strings to get actual keywords
$sql_keywords_filtered = [];
foreach($sql_keywords as $keyword){
if(!empty($keyword) && !is_numeric($keyword)){
$sql_keywords_filtered[] = $keyword;
}
}
// list of forbidden/dangerous keywords
$dangerous_keywords = [
'INSERT',
'UPDATE',
'DELETE',
'RENAME',
'DROP',
'CREATE',
'TRUNCATE',
'ALTER',
'COMMIT',
'ROLLBACK',
'MERGE',
'CALL',
'EXPLAIN',
'LOCK',
'GRANT',
'REVOKE',
'SAVEPOINT',
'TRANSACTION',
'SET'
];
// Contains an array of dangerous keywords found
// If this array is empty, query is safe
$found_dangerous_keywords = array_intersect($dangerous_keywords, $sql_keywords_filtered);
return count($found_dangerous_keywords) > 0;
}
You can check whether there is any DELETE statement in the query by the following code.
if (preg_match('/(DELETE|DROP|TRUNCATE)/',strtoupper($query)) == 0){
/*** Run your query here ***/
}
else {
/*** Do something ***/
}
Note as pointed out by peter it is better to have a seperate MySql user.
There is another method, if you are using the C API, using the mysql_stmt_prepare() method will allow you to query the field_count, which will be non-zero on a select statement.
It also allows you to use proper sql preparation instead.
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
}
I have a problem with a question mark parameter in a prepared statement using PDO. My Query class looks like this (for now, I'm still adding functionality like data limits, custom parameters filtering and automatic detection of supported statements for the driver being used):
// SQL query
class Query {
public $attributes;
// constructor for this object
public function __construct() {
if ($arguments = func_get_args()) {
$tmp = explode(" ", current($arguments));
if (in_array(mb_strtoupper(current($tmp)), ["ALTER", "DELETE", "DROP", "INSERT", "SELECT", "TRUNCATE", "UPDATE"], true)) {
// classify the query type
$this->attributes["type"] = mb_strtoupper(current($tmp));
// get the query string
$this->attributes["query"] = current($arguments);
// get the query parameters
if (sizeof($arguments) > 1) {
$this->attributes["parameters"] = array_map(function ($input) { return (is_array($input) ? implode(",", $input) : $input); }, array_slice($arguments, 1, sizeof($arguments)));
}
return $this;
}
}
}
}
This is the code fragment which executes the query:
$parameters = (!empty($this->attributes["queries"][$query]->attributes["parameters"]) ? $this->attributes["queries"][$query]->attributes["parameters"] : null);
if ($query = $this->attributes["link"]->prepare($this->attributes["queries"][$query]->attributes["query"], [\PDO::ATTR_CURSOR => \PDO::CURSOR_FWDONLY])) {
if ($query->execute((!empty($parameters) ? $parameters : null))) {
return $query->fetchAll(\PDO::FETCH_ASSOC);
}
}
And this is how I call it in my test code:
$c1->addQuery("lists/product-range", "SELECT * FROM `oc_product` WHERE `product_id` IN (?);", [28, 29, 30, 46, 47]);
if ($products = $c1->execute("test2")) {
foreach ($products as $product) {
print_r($product);
}
}
The problem I have is I just see the first product (it's a test against a vanilla OpenCart installation) with id 28. As you can see in my code, if the passed parameter is an array, it gets automatically detected by the lambda I have in place in the Query class constructor, so it gets rendered as a string like 28,29,30,46,47.
Is there a missing parameter in PDO setup I'm missing? Or maybe there's some bug or platform limit in what I'm doing? I know there's some limitations on what PDO can do in regards to arrays, and that's why I pre-implode all arrays for them to be passed like a simple string.
There's some procedures I've seen here in SO which, basically, composes the query string like WHERE product_id IN ({$marks}), where $marks is being dynamically generated using a procedure like str_repeat("?", sizeof($parameters)) but that's not what I'm looking for (I could resort to that in case there's no known alternative, but it doesn't look like a very elegant solution).
My development environment is composed of: Windows 7 x64, PHP 5.4.13 (x86, thread-safe), Apache 2.4.4 (x86) and MySQL 5.6.10 x64.
Any hint would be greatly appreciated :)
A ? placeholder can only substitute for a single literal. If you want an IN clause to accept an arbitrary number of values, you must prepare a new query for each possible length of your array.
E.g., if you want to select ids in array [1, 2], you need a query that looks like SELECT * FROM tbl WHERE id IN (?,?). If you then pass in a three-item array, you need to prepare a query like SELECT * FROM tbl WHERE id IN (?,?,?), and so on.
In other words, you cannot know with certainty what query you want to build/create until the moment you have the data you want to bind to the prepared statement.
This is not a PDO limitation, it is fundamental to how prepared queries work in SQL databases. Think about it--what datatype would the ? be in SQL-land if you said IN ? but had ? stand in for something non-scalar?
Some databases have array-types (such as PostgreSQL). Maybe they can interpret IN <array-type> the same way as IN (?,?,...) and this would work. But PDO has no way of sending or receiving array-type data (there is no PDO::PARAM_ARRAY), and since this is an uncommon and esoteric feature it's unlikely PDO ever will.
Note there is an extra layer of brokenness here. A normal database, when faced with the condition int_id = '1,2,3,4' would not match anything since '1,2,3,4' cannot be coerced to an integer. MySQL, however, will convert this to the integer 1! This is why your query:
$pstmt = $db->prepare('SELECT * FROM `oc_product` WHERE `product_id` IN (?)');
$pstmt->execute(array('28,29,30,46,47'));
Will match product_id = 28. Behold the insanity:
mysql> SELECT CAST('28,29,30,46,47' AS SIGNED INTEGER);
+------------------------------------------+
| CAST('28,29,30,46,47' AS SIGNED INTEGER) |
+------------------------------------------+
| 28 |
+------------------------------------------+
1 rows in set (0.02 sec)
Lambda detects an array and creates coma delimited string from it, and passed argument is treated as string, so the query looks like:
SELECT * FROM tbl WHERE id IN('1,2,3,4')
'1,2,3,4' is one string value for SQL.
If you are expecting only numerical values, you can omit adding them as parameters and simply put them in the query:
$a = [28, 29, 30, 46, 47];
$s = "SELECT * FROM tbl WHERE id IN(".implode(',', array_map('intval', $a)).")";
For different data types, you have to add as many parameter placeholders as you need, and bind every parameter separately.
I have an insert statement that inserts variables collected from a form POST on the previous page. If the variables from the form are not filled in it fails on insert (presumably because it is inserting an empty string...) I have the dataype set to allow NULL values - how do I insert null values if the field was left empty from the form POST?
$query = "
INSERT INTO songs (
userid,
wavURL,
mp3URL,
genre,
songTitle,
BPM
) VALUES (
'$userid',
'$wavFile',
'$mp3File',
'$genre',
'$songTitle',
'$BPM'
)
";
$result = mysql_query($query);
The exact manner depends on if you are writing the query or binding parameters to a prepared statement.
If writing your own, it would look something like this:
$value = empty($_POST['bar']) ? null : $_POST['bar'];
$sql = sprintf('INSERT INTO foo (bar) VALUES (%s)',
$value === null ? 'NULL', "'".mysql_real_escape_string($value)."'");
$result = mysql_query($sql);
The main point is that you need to pass in the string NULL (without quotes) if the value should be null and the string 'val' if the value should be "val". Note that since we are writing string literals in PHP, in both cases there is one more pair of quotes in the source code (this makes one pair in the first case, two pairs in the second).
Warning: When inserting to the database directly from request variables, it is very easy to be wide open to SQL injection attacks. Do not be another victim; read about how to protect yourself and implement one of the universally accepted solutions.
For what I understand when something is not filled the post variable is not set as an empty value but rather not set at all so in php you'd do for example:
$genre = isset($_POST['genre']) ? $_POST['genre'] : NULL;
Here's how I do it. I don't like sending anything to an SQL query right from POST (always sanitize!) in the following cas you just run through the POST vars one by one and assign them to a secondary array while checking for 0 length strings and setting them to NULL.
foreach ($_POST as $key => $value) {
strlen($value)=0 ? $vars[$key] = NULL : $vars[$key] = $value
}
Then you can build your SQL query from the newly created $vars[] array.
As Jon states above, this would be the place to also escape strings, strip code and basically do all your server side validation prior to data being inserted into the db.