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.
Related
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')
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.
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) );
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.
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`.