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.
Related
Is there something that may escape the sanitation in my script or is it safe from most SQL injections? The way I understand it, if you pass query as prepared argument, it does not matter how the query was build, right?
Edit2: I edited the code to reflect the suggestions of binding the $_POST values
$q = $pdo->prepare('SHOW COLUMNS FROM my_table');
$q->execute();
$data = $q->fetchAll(PDO::FETCH_ASSOC);
$key = array();
foreach ($data as $word){
array_push($key,$word['Field']);
}
$sqlSub= "INSERT INTO other_table(";
$n = 0;
foreach ($key as $index){
$sqlSub = $sqlSub.$index.", ";
$n = $n + 1;
}
$sqlSub = $sqlSub.") VALUES (";
for ($i=1; $i<$n;$i++){
$sqlSub = $sqlSub."?, ";
}
$sqlSub = $sqlSub.."?)";
$keyValues = array();
for($i=0;i<n;$i++){
array_push($keyValues,$_POST[$key[$i]]);
}
$q->$pdo->prepare($sqlSub);
q->execute($keyValues);
EDIT: This is how the final query looks like after suggested edits
INSERT INTO other_table($key[0],...,$key[n]) VALUES (?,...,nth-?);
No. The example code shown is not safe from most SQL Injections.
You understanding is entirely wrong.
What matters is the SQL text. If that's being dynamically generated using potentially unsafe values, then the SQL text is vulnerable.
The code is vulnerable in multiple places. Even the names of the columns are potentially unsafe.
CREATE TABLE foo
( `Robert'; DROP TABLE Students; --` VARCHAR(2)
, `O``Reilly` VARCHAR(2)
);
SHOW COLUMNS FROM foo
FIELD TYPE NULL
-------------------------------- ---------- ----
Robert'; DROP TABLE Students; -- varchar(2) YES
O`Reilly varchar(2) YES
You would need to enclose the column identifiers in backticks, after escaping any backtick within the column identifier with another backtick.
As others have noted, make sure your column names are safe.
SQL injection can occur from any external input, not just http request input. You can be at risk if you use content read from a file, or from a web service, or from a function argument from other code, or the return value of other code, or even from your own database... trust nothing! :-)
You could make sure the column names themselves are escaped. Unfortunately, there is no built-in function to do that in most APIs or frameworks. So you'll have to do it yourself with regular expressions.
I also recommend you learn about PHP's builtin array functions (http://php.net/manual/en/ref.array.php). A lot of your code could be quicker to develop the code, and it will probably better runtime performance too.
Here's an example:
function quoteId($id) {
return '`' . str_replace($id, '`', '``') . '`';
}
$q = $pdo->query("SHOW COLUMNS FROM my_table");
while ($field = $q->fetchColumn()) {
$fields[] = $field;
}
$params = array_intersect_key($_POST, array_flip($fields));
$fieldList = implode(",", array_map("quoteId", array_keys($params)));
$placeholderList = implode(",", array_fill(1, count($params), "?"));
$sqlSub = "INSERT INTO other_table ($fieldList) VALUES ($placeholderList)";
$q = $pdo->prepare($sqlSub);
$q->execute($params);
In this example, I intersect the columns from the table with the post request parameters. This way I use only those post parameters that are also in the set of columns. It may end up producing an INSERT statement in SQL with fewer than all the columns, but if the missing columns have defaults or allow NULL, that's okay.
There is exactly one way to prevent SQL injection: to make sure that the text of your query-string never includes user-supplied content, no matter how you may attempt to 'sanitize' it.
When you use "placeholders," as suggested, the text of the SQL string contains (probably ...) question marks ... VALUES (?, ?, ?) to indicate each place where a parameter is to be inserted. A corresponding list of parameter values is supplied separately, each time the query is executed.
Therefore, even if value supplied for last_name is "tables; DROP TABLE STUDENTS;", SQL will never see this as being "part of the SQL string." It will simply insert that "most-unusual last_name" into the database.
If you are doing bulk operations, the fact that you need prepare the statement only once can save a considerable amount of time. You can then execute the statement as many times as you want to, passing a different (or, the same) set of parameter-values to it each time.
This question already has answers here:
How can I bind an array of strings with a mysqli prepared statement?
(7 answers)
Closed 1 year ago.
Using bind_param on all my queries, I now want to use an IN(?) where the number of elements in the list can vary.
The SQLout function I'm using here basically does a $sql_db->prepare, ->bind_param, ->execute(), ->store_result(), ->bind_result
// the code below does not work as the query only matches on element 'a':
$locations = ('a','b','c','d','e');
SQLout ("SELECT Name FROM Users WHERE Locations IN (?)",
array('s', $locations), array(&$usrName));
// the code below does work as a brute-force method,
// but is not a viable solution as I can't anticipate the number of elements in $locations going forward:
SQLout ("SELECT Name FROM Users WHERE Locations IN (?,?,?,?,?)",
array('sssss', $locations[0],$locations[1],$locations[2],$locations[3],$locations[4]), array(&$usrName));
Has anyone come up with a more elegant solution to this?
This is one place placeholders fall on their faces. Minus the auto-escaping, they're almost literally just a string replacement operation internally, meaning that if you have WHERE Locations IN (?), and pass in 1,2,3,4, you'll get the equivalent of
WHERE Locations IN ('1,2,3,4') // note, it's a string, not individual comma-separated integers
logically equivalent to
WHERE Locations = '1,2,3,4' // again, just a string
instead of the intended
WHERE Locations = 1 OR Locations = 2 OR Locations = 3 OR Locations = 4
The only practical solution is to build your own list of comma-separated placeholders (?), e.g:
$placeholders = implode(',', array_fill(0, count($values), '?'));
$sql = "SELECT Name FROM Users WHERE Locations IN ($placeholders)";
and then bind your parameters are usual.
As Hazmat said, you need to build up the parameters and then pass them by calling call_user_func_array on the prepared statement, but slightly closer to working code than his example :)
//In the calling code
$queryString = "SELECT Name FROM Users WHERE Locations IN (";
$queryString .= getWhereIn($locations);
$queryString .= " )";
$parametersArray = array();
foreach($locations as $location){
$parameter = array();
$parameter[0] = 's'; //It's a string
$parameter[1] = $location;
$parametersArray[] = $parameter;
}
//This is a function in a class that wraps around the class mysqli_statement
function bindParameterArray($parameterArray){
$typesString = '';
$parameterValuesArray = array();
foreach($parameterArray as $parameterAndType){
$typesString .= $parameterAndType[0];
$parameterValuesArray[] = $parameterAndType[1];
}
$finalParamArray = array($typesString);
$finalParamArray = array_merge($finalParamArray, $parametersArray);
call_user_func_array(array($this->statement, "bind_param"), $finalParamArray);
}
function getWhereIn($inArray){
$string = "";
$separator = "";
for($x=0 ; $x<count($inArray) ; $x++){
$string .= $separator."?";
$separator = ", ";
}
return $string;
}
You can "build" in IN clause before you prepare/bind it.
$sql = 'SELECT Name FROM Users WHERE Locations IN (' . implode(array_fill(0, count($locations), '?')) . ')';
Then you can use call_user_func_array to bind the parameters, without ever knowing how many there are.
// Parameters for SQLOut
$params = array(
# The SQL Query
$sql,
# The params for bind_param
array(str_repeat('s', count($locations))),
# The params for bind_result
array(&$usrName)
);
// Add the locations into the parameter list
foreach($locations as &$loc){
// not sure if this is needed, but bind_param
// expects its parameters to be references
$params[1][] = &$loc;
}
// Call your function
call_user_func_array('SQLout', $params);
Note: This is untested
IN is usually slow and not prepared statement friendly. The better solution is to build a table of the items that would be in the IN and use a JOIN to get the same effect.
Has anyone come up with a more elegant solution to this?
Sure. I have.
Mysqli is practically unusable with prepared statements, especially with such complex cases.
So, it's better to get rid of prepared statements and implement your own placeholders with support of all real life cases that a developer can meet.
safeMysql (a creation of mine) has a solution you're looking for (and also solutions for a dozen other headaches as well).
In your particular case, it would be as easy as this single line of code
// the code works alright:
$locations = array('a', 'b', 'c', 'd', 'e');
$usrName = $db->getCol("SELECT Name FROM Users WHERE Locations IN (?a)", $locations);
Unlike ugly codes you can get while playing with some PHP and API functions (and still get disturbing warnings depends on the PHP version you're using at the moment), this code is neat and readable. This is an important matter. You can tell what this code does even after a year has passed.
I have a sql query that is generated using php. It returns the surrogate key of any record that has fields matching the search term as well as any record that has related records in other tables matching the search term.
I join the tables into one then use a separate function to retrieve a list of the columns contained in the tables (I want to allow additions to tables without re-writing php code to lower ongoing maintenance).
Then use this code
foreach ($col_array as $cur_col) {
foreach ($search_terms_array as $term_searching) {
$qry_string.="UPPER(";
$qry_string.=$cur_col;
$qry_string.=") like '%";
$qry_string.=strtoupper($term_searching);
$qry_string.="%' or ";
}
}
To generate the rest of the query string
select tbl_sub_model.sub_model_sk from tbl_sub_model inner join [about 10 other tables]
where [much code removed] or UPPER(tbl_model.image_id) like '%HONDA%' or
UPPER(tbl_model.image_id) like '%ACCORD%' or UPPER(tbl_badge.sub_model_sk) like '%HONDA%'
or UPPER(tbl_badge.sub_model_sk) like '%ACCORD%' or UPPER(tbl_badge.badge) like '%HONDA%'
or UPPER(tbl_badge.badge) like '%ACCORD%' group by tbl_sub_model.sub_model_sk
It does what I want it to do however it is vulnerable to sql injection. I have been replacing my mysql_* code with pdo to prevent that but how I'm going to secure this one is beyond me.
So my question is, how do I search all these tables in a secure fashion?
Here is a solution that asks the database to uppercase the search terms and also to adorn them with '%' wildcards:
$parameters = array();
$conditions = array();
foreach ($col_array as $cur_col) {
foreach ($search_terms_array as $term_searching) {
$conditions[] = "UPPER( $cur_col ) LIKE CONCAT('%', UPPER(?), '%')";
$parameters[] = $term_searching;
}
}
$STH = $DBH->prepare('SELECT fields FROM tbl WHERE ' . implode(' OR ', $conditions));
$STH->execute($parameters);
Notes:
We let MySQL call UPPER() on the user's search term, rather than having PHP call strtoupper()
That should limit possible hilarious/confounding mismatched character set issues. All your normalization happens in one place, and as close as possible to the moment of use.
CONCAT() is MySQL-specific
However, as you tagged the question [mysql], that's probably not an issue.
This query, like your original query, will defy indexing.
Try something like this using an array to hold parameters. Notice % is added before and after term as LIKE %?% does not work in query string.PHP Manual
//Create array to hold $term_searching
$data = array();
foreach ($col_array as $cur_col) {
foreach ($search_terms_array as $term_searching) {
$item = "%".strtoupper($term_searching)."%";//LIKE %?% does not work
array_push($data,$item)
$qry_string.="UPPER(";
$qry_string.=$cur_col;
$qry_string.=") LIKE ? OR";
}
}
$qry_string = substr($qry_string, 0, -3);//Added to remove last OR
$STH = $DBH->prepare("SELECT fields FROM table WHERE ". $qry_string);//prepare added
$STH->execute($data);
EDIT
$qry_string = substr($qry_string, 0, -3) added to remove last occurrence of OR and prepare added to $STH = $DBH->prepare("SElECT fields FROM table WHERE". $qry_string)
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`.