Quicker way of sanitising and constructing query from PHP posted form - php

I have a form which has over 100 fields (most of which are checkboxes) and although not every field is always filled in, a large number will be. The thought of typing out each field to sanitise and construct the MySQL query is making me weep so I figure there must be a way to do this using a PHP loop.
I have managed to sanitise the form without writing out every element in the $_POST array and have it stored in $formarray in the form of $key=>$value. $key is not numerical or sequential and needs to be retained. The value of $key is the same as the column name in the database table that $value will be written to.
I want to be able to construct the MySQL query to insert the contents of $formarray into the MySQL table using a PHP loop. Is this possible?

It is possible, but not recommended, unless you are very careful. Making this loop approach secure may consume as much time as doing all the typing for all form fields, but your solution will be more roboust and modular.
You should create a mapping array wich contains directives on validating and formatting a certain value, something like this:
$validator = array(
'user_name' => array('mask' => "'%s'", 'escape' => true, 'boolean' => false),
'user_age' => array('mask' => "%d", 'escape' => false, 'boolean' => false),
'has_car' => array('mask' => "%d", 'escape' => false, 'boolean' => true)
);
Then you iterate this validator array, and check if the key exists in your posted data, and apply the directives on the value, and collect all the SQL sets in a separate array:
$set = array();
foreach ($_POST as $key => $dirs)
{
if ($dirs['boolean'])
{
$val = isset($_POST[$key]) ? 1 : 0;
$set[] = sprintf("%s=".$dirs['mask'],$key,$val);
}
elseif (isset($_POST[$key]))
{
$val = $dirs['escape'] ? addslashes($_POST[$key]) : $_POST[$key];
$set[] = sprintf("%s=".$dirs['mask'],$key,$val);
}
}
After this you can create the query by imploding the set:
$sql = "INSERT INTO table SET ".implode(",",$set);
My approach above may not suite you actual needs, but is more of a concept you can use to handle large forms on server side. You can define more complex validatons, and make it handle arrays too, not just checkboxes and text fields like this example.
Always be on your guard, your form is the fornt door to your precious data, and if you go cheap, you'll be sorry!

You may want to look into using PDO and prepared statements. One of the great things about it is that it will handle escaping "questionable" characters for you.

You could create a whitelist of columns by getting the column names directly from the database. You can then validate your posted fields against that list. Using prepared statements via PDO, you can easily build up an SQL statement to do the insert. Prepared statements save you from having to escape your data, but you will probably want to perform some validation prior to inserting the values into the database.
Should you need it, the SHOW COLUMNS statement provides other information about the columns, so you could, for example, use it to grab only BOOL columns.
Here's a completely untested example:
$dbh = new PDO('mysql:host=localhost;dbname=test', $user, $pass);
$valid = array();
foreach($dbh->query('SHOW COLUMNS FROM mytable') as $col) {
$field = $col['Field'];
if( isset($_POST[ $field ]) ) {
// Insert suitable validation here.
$validated_value = get_validated_value( $_POST[ $field ] );
$valid[ $field ] = $validated_value;
}
}
$cols = implode(',', array_keys($valid));
$params = implode(',', array_fill(0, count($valid), '?'));
$sql = "INSERT INTO mytable ($cols) VALUES ($params)";
$stmt = $dbh->prepare( $sql );
$stmt->execute( array_values($valid) );

Related

A form can return varying number of values, trying to use the returned values in doctrine query

I have saved a bunch of parsed messages in a db, and I want to be able to pull out results depending on user input. I would like to sort by 1-4 values (more options later on), username, id, email or date and time. I've been using doctrine to do my other queries, but this one, I just can't figure out.
My issue is, I don't see how I could possibly check which values are passed into the method and only query for those.
For example:
User enters "bob" into username and "bob#example.com" into the email field. For this scenario I would need doctrine to look something like this:
$queryBuilder
->select('*')
->from('m2m_messages')
->where('username = ' . $queryBuilder->createNamedParameter($params[username]))
->andWhere('email = ' . $queryBuilder->createNamedParameter($params[email]))
$query = $queryBuilder->execute();
But I would like it to change, without creating all the copies separately for each occasion... If I wanted to specify the ID and email, it should only take those values.
I am passing the values in through an array which only contain the search terms I need to use in the sql. For above example it would look something like this:
$params [
username => 'bob',
email => 'bob#example.com'
]
I haven't found a way to implement an if(isset) into my code that would allow me to automatically only use the values I passed into the method.
My goal is to be able to enter details into any number of fields, the values entered get passed in an array, and any combination of them is accepted and crafted into the sql script, without having to write 20+ different sql scripts separately.
Something like this?
$queryBuilder
->select('*')
->from('m2m_messages');
if (count($params) > 0) {
$isFirst = true;
foreach ($params as $paramKey => $paramValue) {
if ($isFirst) {
$queryBuilder->where(sprintf('%1$s = %2$s', $paramKey, $queryBuilder->createNamedParameter($paramValue)));
$isFirst = false;
} else {
$queryBuilder->andWhere(sprintf('%1$s = %2$s', $paramKey, $queryBuilder->createNamedParameter($paramValue)));
}
}
}
$query = $queryBuilder->execute();
The concept is to iterate through your parameters and add the where conditions supplied in $params.

How to allow SELECT queries and prevent others?

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.

A more efficient way of binding a big insert or update?

Ok so im new to binding, here is some code that works. I learned this format from a tutorial but i imagine there is more efficent ways to do it. In my example there is 4 names but in reality i will be doing a lot of inserts and updates in a project im working on that will have 20 or so fields. I like this approach for its clarity but obviously when your talking 20 fields or more it does take a lot of real estate. Lets look at my code first.
Here are the functions it uses:
// prepare the statement
public function query($query){
$this->stmt = $this->dbh->prepare($query);
}
public function bind($param, $value, $type = null){
if (is_null($type)) {
switch (true) {
case is_int($value):
$type = PDO::PARAM_INT;
break;
case is_bool($value):
$type = PDO::PARAM_BOOL;
break;
case is_null($value):
$type = PDO::PARAM_NULL;
break;
default:
$type = PDO::PARAM_STR;
}
}
// run the binding process
$this->stmt->bindValue($param, $value, $type);
}
// execute the prepared statement
public function execute(){
return $this->stmt->execute();
}
and now the actual code
$database->query("
INSERT INTO users(
user_name,
user_password_hash,
user_email,
user_activation_hash
)
VALUES(
:user_name,
:user_password_hash,
:user_email,
:user_activation_hash
)
");
// bind the values
$database->bind(":user_name", "$this->user_name");
$database->bind(":user_password_hash", "$this->user_password_hash");
$database->bind(":user_email", "$this->user_email");
$database->bind(":user_activation_hash", "$this->user_activation_hash");
// execute the statement and insert the values into the database
$database->execute();
it just cries out for a loop, especially since i have a habit of calling post fields, input fields, variables and placeholders the same name, not sure if that is a good or a bad thing but i find its helpful for me when dealing with large forms which i will be.
in any case i could do something like this:
$placeholder_array = array(
"user_name" => "\$this->user_name",
"user_password_hash" => "\$this->user_password_hash",
"user_email" => "\$this->user_email",
"user_activation_hash" => "\$this->user_activation_hash"
);
// well use this copy to edit the array keys and keep original for the binding
$placeholder_copy = $placeholder_array;
// turn the array into a string i.e user_name, user_password_hash....
$fields = implode (", ", array_keys($placeholder_array));
// foreach to add : placeholder prefix for binding
foreach ($placeholder_copy as $key => $value){
$placeholder_copy [':'.$key] = $value;
unset($placeholder_copy[$key]);
}
// turn the copy array which has prefix :user_name into a string
$placeholders = implode (", ", array_keys($placeholder_copy));
$database->query("
INSERT INTO users($fields)
VALUES($placeholders)
");
// bind the values
foreach ($placeholder_copy as $bind_values => $value){
echo '$database->bind("'.$bind_values.'", "'.$value.'");' . "<br />";
}
// execute the statement and insert the values into the database
$database->execute();
i could then turn this into a function with parameters for passing in the associative array and the table name to keep my main code much cleaner.
Now imagine i am going to be doing any amount of these as the project im working on involves tons of big forms submitting data to users. I'm new to PDO and trying to grasp it so there maybe a simpler way of structuring these types of queries, i had a look on google and stackflow but i didnt really get what they were doing so i thought doing my own one would allow people to explain to me better what is going on, i would rather get this right starting my project than have to go back and change everything later. So is there a better approach or is this one ok?
Really appreciate any feedback and im glad now i took peoples advice on here and made the move to PDO.
No, sadly, but PDO offers no help for the matter.
Yet your own approach doesn't seem to me an efficient one either.
First of all, let me indicate that your set of functions is quite useless. I understand no such direct rewriting of API functions. PDO already doing all these things.
With raw PDO you can have even more concise code:
$stm = $pdo->prepare("INSERT INTO users VALUES(NULL,?,?,?,?)");
$data = array($this->user_name,
$this->user_password_hash,
$this->user_email,
$this->user_activation_hash
);
$stm->execute($data);
Regarding your dynamical query building, I find it too much bloated yet still insecure. Some flaws are
there is no point in bothering with ":named" placeholders as they are supposed to be human-readable but no human supposed to read them here. Not to mention PDO will convert them back into ?s before sending to mysql
another objection against named placeholders - while Mysql (as well as HTTP) can let you have a space in a field name all right, a placeholder with a space will just crash your query.
there is no real benefit in this code from your approach of the equal names - you are bound to write them by hand a dozen times each, mind you.
yet if it would be used, you have no white list to check your field list against, which could be a great security breach
too much code
no support for UPDATE query which could be extremely handy as well (if you are using mysql)
when you put this code into function, you will slip to the same bad grounds as others, tempted by the possibility of saving you whole two words!
Here is my earlier approach for the matter:
$allowed = array(
"user_name", "user_password_hash", "user_email", "user_activation_hash"
);
$sql = "INSERT INTO users SET ".pdoSet($allowed, $values);
$stmt = $dbh->prepare($sql);
$stmt->execute($values);
And here goes my current approach, the best of the best (though implemented using mysqli, not PDO), using custom placeholder for the array data:
$placeholder_array = array(
"user_name" => $this->user_name,
"user_password_hash" => $this->user_password_hash,
"user_email" => $this->user_email,
"user_activation_hash" => $this->user_activation_hash
);
$db->query("INSERT INTO users SET ?u", $placeholder_array);
or, in case of direct connection between form fields and SQL columns
$allowed = array(
"user_name", "user_password_hash", "user_email", "user_activation_hash"
);
$insert = $db->filterArray($_POST,$allowed);
$db->query("INSERT INTO users SET ?u", $insert);
This way let me use INSERT IGNORE, INSERT DELAYED, INSERT.. ON DUPLICATE UPDATE, UPDATE, UPDATE with join, and innumerable other options, supporting full SQL language.
What I've done recently and continuing to improve, is building a helper class to simplify PDO based SQL statements in my applications.
Lets take a tour with your example. You want to insert data into a user table:
INSERT INTO users(
user_name,
user_password_hash,
user_email,
user_activation_hash
) VALUES(
:user_name,
:user_password_hash,
:user_email,
:user_activation_hash
)
For inserting a single record, my SQL is constructed like this:
function myInsertSingle($PDO, $table, $ins_array) {
$SQL = "INSERT INTO `".$table."` (".dbFieldList($ins_array)
.") VALUES (".dbValuePList($ins_array).")";
...continued with preparing.
$PDO: My PDO connection.
$table: The table I want to insert to.
$ins_array: A structured array for inserting data.
For your example, the array $ins_array will look like this:
$ins_array = array(
"user_name" => "your_user",
"user_password_hash" => "your_pw_hash",
"user_email" => "your#mail.xyz",
"user_activation_hash" => "your_other_Hash"
);
Notice the similarity to your array!
Two functions are involved. The first one gives me a list of (escaped) fieldnames.
function dbFieldList($fields) {
$set = '';
foreach ($fields as $field => $item) {
$set .= "`".$field."`,";
}
return rtrim($set, ',');
}
Basically from the above array example, the function returns
`user_name`, `user_password_hash`, `user_email`, `user_activation_hash`
...which is the list of the fields needed in the INSERT-statement.
The other one does something similar for the values.
function dbValuePList($fields) {
$set = '';
foreach ($fields as $field => $item) {
$set .= ":".$field.",";
}
return rtrim($set, ',');
}
You guessed it, the result looks like this:
:user_name, :user_password_hash, :user_email, :user_activation_hash
Now you have your SQL statement which you can prepare. To bind the values, if I understand your class correctly, just use a loop which looks like this:
foreach ($ins_array as $field => $item) {
$PDO->bind(':'.$field,$item);
}
Now you can execute the statement.
Summary. Basically with my approach a single INSERT statement is reduced to this:
$ins_array = array(
"user_name" => "your_user",
"user_password_hash" => "your_pw_hash",
"user_email" => "your#mail.xyz",
"user_activation_hash" => "your_other_Hash"
);
myInsertSingle($PDO, 'your_table', $ins_array);
If this looks just like another piece of overhead, keep in mind, I use these functions in other SQL statements like SELECT, UPDATE, DELETE and so on.

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.

Safely using prepared statements to query database

I'm trying to write a function that is versatile in the queries it is allowed to make, but also safe from injection. The code below throws an error as is, but if I run it with 'name' instead of ':field' it works fine.
$field = "name";
$value = "joe";
function selectquery($field, $value)
{
global $dbcon;
$select = $dbcon->prepare('SELECT * FROM tester1 WHERE :field = :value');
if($select->execute(array(':field' => $field, ':value' => $value)));
{
$row = $select->fetch();
for ($i=0; $i<3; $i++)
{
echo $row[$i]."\n";
}
}
}
How would I allow the table/fields/values to be changed, without allowing injection attacks? mysql_real_escape_string() seems kind of like a step backwards. Any ideas?
I may be mistaken, but I don't believe you can supply fields as parameters in PDO.
Why not just specify it as argument to the function? Unlike the data being supplied by the user, the fields are finite, well defined and don't change often. As in
selectquery('name',$value);
and have your query be
$field = "name";
$value = "joe";
function selectquery($field, $value)
{
global $dbcon;
$select=$dbcon->prepare("SELECT * FROM tester1 WHERE $field = :value");
if($select->execute(array(':value' => $value)));
//etcetera
Since you're supplying the field name for the function call yourself, this is safe unless you're worried you are going to attack yourself with SQL injection.
If for some odd reason the name of the field is coming from user input, you could make an array of allowed fields. That way, you're safe from injection because the values can only come from your array. I don't know why the field name would be coming from user input and thus be untrusted, unless perhaps you're making an API? Other wise there's probably a better way to achieve the goal.
Anyhow, this would be a potential solution, to use a whitelist for table names:
$field = "name";
$value = "joe";
$allowed_fields=array('name','other_name','sandwich');
function selectquery($field_name, $value)
{
global $dbcon,$allowed_fields;
if(!in_array($field_name,$allowed_fields)){ return false; }
else{ $field=$field_name; }
$select=$dbcon->prepare("SELECT * FROM tester1 WHERE $field = :value");
if($select->execute(array(':value' => $value)));
//etcetera
Use MDB2 autoExecute
http://pear.php.net/manual/en/package.database.mdb2.intro-auto.php
<?php
// Once you have a valid MDB2 object named $mdb2...
$table_name = 'user';
$fields_values = array(
'id' => 1,
'name' => 'Fabien',
'country' => 'France'
);
$types = array('integer', 'text', 'text');
$mdb2->loadModule('Extended');
$affectedRows = $mdb2->extended->autoExecute($table_name, $fields_values,
MDB2_AUTOQUERY_INSERT, null, $types);
if (PEAR::isError($affectedRows)) {
die($affectedRows->getMessage());
}
?>
Database identifiers (column names, table names and database names) can not and should not be escaped, therefore you can't use them in SQL prepared queries.
Sometimes you might need to backtick those identifiers though (use ` for MySQL and " for SQLite).
Binding a variable binds it as data, specifically to prevent it from changing the syntax of the query. Furthermore, having a fixed syntax allows engines to analyze prepared queries once and then run them quickly for each set of values. I would encourage you not to build a hand-holding layer on top of SQL, but if you must, consider a preg_replace('/\W/', '', $field).
PHP Data Objects unfortunately doesn't expose a method to quote a field identifier.
As an alternative, PEAR::MDB2 (spiritual predecessor to PHP Data Objects) has a ->quoteIdentifier() option which allows you to achieve what you want in a safe manner.
function selectquery($field, $value)
{
global $dbcon;
$select = $dbcon->prepare('SELECT * FROM tester1 WHERE ' . $dbcon->quoteIdentifier($field) . ' = :value');
if($select->execute(array('field' => $field, 'value' => $value)));
{
$row = $select->fetchRow();
for ($i=0; $i<3; $i++)
{
echo $row[$i]."\n";
}
}
}
I understand that this solution is less than optimal (changing abstraction layer in the middle of developing a project is cumbersome) but unfortunately, PDO provides no safe way of doing what you want to do.
Seconding Andrew Moore's response: the only way to go is identifier quoting, and PDO doesn't provide the necessary method. Rather than use MDB2, you might just want to borrow its implementation of identifier quoting. The function is simple enough that you should be able to write your own and vet it for bugs fairly easily.
Split the input string on . into a list of parts (there might be only one)
For each part:
Replace all ` with ``.
Add a ` to the beginning and to the end unless the part is empty.*
Join the parts with ..
As an example, quote_identifier("one two.three") should be `one two`.`three` -- pretty straightforward.
For extra safety you could also verify that the string doesn't contain any characters that are illegal even in quoted identifiers (particularly nulls, see the MySQL docs) but in truth MySQL should catch those. MDB2 doesn't bother.
*: This check is necessary because .columnname is legal, and should quote to .`columnname` and not ``.`columnname`.

Categories