PDO not substituting parameters properly - php

I'm making a blog based on PHP and MySQL (using PDO to connect). I have written a function (below) that will retrieve blog posts and return them, but order by isn't working when I pass it by reference:
<?php
/**
* Get blog posts, filtered and sorted to taste.
* #return array Array of rows - each row is an array indexed by column name.
* #param string $inFunction What to select posts by - id, date or tag(s).
* #param string $inFilter Filter data to select posts by - id no., date(s) or tag(s). If you are filtering by date and only specify one, it will be taken to be the 'newer than' date.
* #param string $inOrder How to sort posts. This parameter is fed directly into the query and therefore should be raw SQL.
* #param object $inDatabase Database handle to pass to the SQL query.
*/
function getBlogPosts($inFunction, $inDatabase, $inFilter, $inOrder) {
switch ($inFunction) {
case "permalink": {
$query = $inDatabase->prepare("select * from blog_posts where permalink = :permalink");
$query->bindValue(":permalink", $inFilter);
$query->execute();
$result = $query->fetch();
return new BlogPost($result["id"], $result["title"], $result["permalink"], $result["post_full"], $result["post_sample"], $result["tags"], $result["timestamp"]);
break;
}
case "number": {
$query = $inDatabase->prepare("select * from blog_posts
order by :order
limit :limit_start , :limit_end");
$query->bindParam(":order", $inOrder);
$splitLimits = explode(", ", $inFilter);
if (sizeOf($splitLimits) === 1)
$splitLimits[] = 1; // First post
$limitEnd = $splitLimits[0] + $limitStart;
$limitStart = $splitLimits[1] - 1;
$query->bindValue(":limit_start", (int) $limitStart, PDO::PARAM_INT);
$query->bindValue(":limit_end", (int) $limitEnd, PDO::PARAM_INT);
$query->debugDumpParams();
$query->execute();
$results = $query->fetchAll(PDO::FETCH_ASSOC);
$return = array();
foreach ($results as $result) {
$return[] = new BlogPost($result["id"], $result["title"], $result["permalink"], $result["post_full"], $result["post_sample"], $result["tags"], $result["timestamp"]);
}
return $return;
break;
}
case "id": {
$query = $inDatabase->prepare("select * from blog_posts where id = :id order by :order");
$query->bindParam(":id", $inFilter);
$query->bindParam(":order", $inOrder);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC); // Prevents duplicate results when using loops (foreach, while etc.)
break;
}
case "date": {
$splitdate = explode(", ", $inFilter);
$query = $inDatabase->prepare("select * from blog_posts
where (date_posted > :newerthan_date)
and (date_posted <= :olderthan_date)
order by :order");
if (sizeof($splitdate) === 1) {
$splitdate[] = date("Y-m-d");
}
$query->bindParam(":newerthan_date", $splitdate[0]);
$query->bindParam(":olderthan_date", $splitdate[1]);
$query->bindParam(":order", $inOrder);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
break;
}
case "tag": {
$tags = explode(", ", $inFilter);
$insert = "";
foreach ($tags as $key => $tag) {
if ($key === 0) {
$insert .= "where tags like :tag_{$key}";
}
else {
$insert .= " or tags like :tag_{$key}";
}
}
$query = $inDatabase->prepare("select * from blog_posts
{$insert}
order by :order");
foreach ($tags as $key => $tag) {
$query->bindValue(":tag_{$key}", '%^' . $tag . '^%');
}
$query->bindParam(":order", $inOrder);
$query->execute();
return $query->fetchAll(PDO::FETCH_ASSOC);
break;
}
}
}
On the main page, $results = getBlogPosts("number", $sql_conn, "10", "timestamp desc"); is called and a foreach loop iterates through the posts. The problem is that PDO does not seem to be applying the :order parameter; whatever I put for $inOrder it always has the same order. If I go and directly change the statement within the getBlogPosts function to have order by timestamp desc instead of order by :order, it works fine.
I'm stumped - any ideas?

"This parameter is fed directly into the query and therefore should be raw SQL." => the comment is correct, the code is not, fix this. The reason is you can specify strings/numbers/etc. with parameters, but not identifiers (column names etc.).
What your query does it this:
SELECT ... FROM ... ORDER BY 'columnname';
Rather then:
SELECT ... FROM ... ORDER BY columnname;
So. it sorts by the string 'columnname', not the value in the field with the same name, with is the same for every row, so no sorting takes place (you might as well ORDER BY 1. The solution here is to add that order by clause as raw MySQL as the docblock comment states. If you want more control over it / prevent nastiness there, you could provide a whitelist of allowable order by clauses and refuse others.

Related

SQLITE build query string from values passed

Hi have a function that gets passed strings via the below;
getFilterResults($id, $limit, $misc);
and in my function i connect to SQLite DB and build a query based on the values passed through, like so;
function getFilterResults($id, $limit, $misc) {
$dbConnect = new SQLite3(database);
if(!empty($limit) && !empty($id) && !empty($misc)){
$buildString = ('SELECT * FROM fields ORDER BY id DESC');
}else{
//no params filters arrived so dont filter - aka get all results
$buildString = ("SELECT * FROM fields ORDER BY id DESC");
}
$query = $dbConnect->query($buildString);
//...
}
Issue is how do I build the query if some of the values are empty and I have to decide which value uses/starts of the 'WHERE' query..obviously the first value return that is not null starts of the 'WHERE'..
I can see the long way would be to loop through each one and build - once the first value found that is not null and kick off from there but that seems unpractical and not good practice.
//check if limit set..
if($limit) {
$doLimit = 'LIMIT '.$limit.'';
}
WHERE 'id = '.id.' AND misc = '.$misc.' '.$doLimit.'
Your function taking in ($id, $limit, $misc) really prevents you from being able to do anything else than what you already are... I would recommend using prepared statements though
// Stores values for a prepared statement
$values = [];
$sqlite = new SQLite3(/* ... */);
$query = 'SELECT * FROM fields WHERE id = ? AND misc = ? ORDER BY id DESC';
$values[] = $id;
$values[] = $misc;
if ($limit) {
$query .= ' LIMIT ?';
$values[] = $limit;
}
// Use prepare and let SQLite3 escape the values for you
$stmt = $sqlite->prepare($query);
foreach ($values as $idx => $val) {
$stmt->bindValue($idx, $val);
}
$result = $stmt->execute();

Variable sql query depending on number of search parameter

I need to do a sql query in php for search some entries (so using WHERE). But the field used to search could be of variable number.
I have a page with a search form, with 4 Field. It sends via POST the fields to a search.php that make a query:
$gomme_sql = $data->query("SELECT * FROM table WHERE parameter1 = '$_POST['name1']' AND parameter2 = '$_POST['name2']' ORDER BY id ASC");
But I don't know which field are filled. So, if I don't enter anything in field1 from the search form, I shouldn't have parameter1 = '$_POST['name1']' in the WHERE query.
Have you any idea how to obtain this?
Thank you
You can check the post data before appending that clause to the query in a way like this:
edit: adding additional check:
$sql="select something from someTable ";
if(!empty($_POST['name1']) || !empty($_POST['name2'])) // add as many as you like
{
$sql.=" where ";
if(!empty($_POST['name1']))
{
$sql.="parameter1= $_POST['name1']";
}
// etc etc...
}
$sql.=" ORDER BY id ASC";
and so on.
Having said that, please, please use prepared statements with this sort of input from the user. This is SUPER open to sql injection. Please do read this: How can I prevent SQL injection in PHP?
You can write generic sql select function like this , if you need more complex SQL just modify it.
<?php
function sqlSelect($table, $sel, $wh = '', $groupby = '', $order = '', $add = '') {
$tb = $table;
if (is_array($table)) {
$tb = implode(',', $table);
}
if ($wh) {
if (is_array($wh)) {
$w = array();
foreach ($wh as $k => $v) {
$v = mysqli_real_escape_string($v);
if (is_null($v))
$w [] = "$k=null ";
else
$w [] = "$k ='$v'";
}
$wh = 'where ' . implode(' and ', $w);
}else {
$wh = "where $wh";
}
}
if ($groupby)
$groupby = "group by $groupby";
if ($order)
$order = "order by $order";
$sql = "select $sel from $tb $wh $groupby $order $add ";
return $sql;
}
//set _GET as this is console test
$_GET['name1']='Bob';
$where = array(
'name1'=>$_GET['name1']
);
echo sqlSelect('sometable' , '*' , $where) ."\n";
// select * from sometable where name1 ='Bob'
//or some complex stuff
echo sqlSelect('persons', "age,status" , array('name'=>'Maria' , 'likes'=>'PHP') , null, 'age' , 'limit 20');
//select age,status from persons where name ='Maria' and likes ='PHP' order by age limit 20

PDO Query Variable Issue?

In the following queries, I'm using entry_id values to from exp_relationships to find out the smallest value (a year) in exp_channel_data. I'll give you some dummy data.
entry_id 1 has a year of 2014
entry_id 2 has a year of 2012
The first query generates "1, 2" which I save for the next one to provide the WHERE parameters. If I were to type in "1, 2" instead of :showid, it works correctly and displays the 2012 since that is the first one in the ORDER BY I have set. But when the query runs as it is written below, it displays the 2014.
$sql = 'SELECT DISTINCT parent_id FROM exp_relationships WHERE child_id = :entry_id AND grid_field_id IN (90, 91)';
$stmt = $conn->prepare($sql);
$showid = "";
try {
$stmt->execute(array('entry_id' => {entry_id}));
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $row) {
$showid .= $row['parent_id'].", ";
}
$showid = substr($showid, 0, -2);
} catch(PDOException $e) { error_log($e->getMessage(),0); }
$sql = 'SELECT field_id_16 FROM exp_channel_data WHERE entry_id IN (:showid) ORDER BY field_id_16 ASC LIMIT 1';
$stmt = $conn->prepare($sql);
$year = "";
try {
$stmt->execute(array('showid' => $showid));
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $row) {
$year = $row['field_id_16'];
echo $year;
}
} catch(PDOException $e) { error_log($e->getMessage(),0); }
This is because defaultly it will cast $showid to its primitive type, string or int. In you case I guess if will be the string 1, 2. This way you cannot just inject variable value in IN to simulate more than one value. Each placeholder is one value, and IN (:placeholder) will refer to only one thing. You need either to split as much placeholders as needed, i.e. in this particular case 2 placeholders IN (?, ?) and then implode your $showid string into array. Or implode $showid into array and then split it into the IN clause, which I consider the worse way of both.

SQL Injection - is this query secure?

I have a a page that appends different parameters to the URL that are used for the query.
For example
http://www.example.com/search.php?category=Schools&country[]=Belgium&country[]=Czech+Republic
My code is like this
if(isset($_GET['country'])){
$cties = "'" . implode("','", $_GET['country']) . "'";
}
else {
$cties = "'Albania','Andorra','Austria','Belarus','Belgium','Bosnia & Herzegovina','Bulgaria','Croatia','Czech Republic','Denmark','Estonia','Faroe Islands','Finland','France','Germany','Gibraltar','Great Britain','Greece','Hungary','Iceland','Ireland','Isle of Man','Italy','Latvia','Liechtenstein','Lithuania','Luxembourg','Macedonia','Malta','Moldova','Monaco','Montenegro','Netherlands','Norway','Poland','Portugal','Serbia','Romania','San Marino','Slovakia','Slovenia','Spain','Sweden','Switzerland','Ukraine','United Kingdom'";
}
if(isset($_GET['category'])){
$cat = $_GET['category'];
}
else{
$cat = " ";
}
try{
// create the Prepared Statement
$stmt = $con->prepare("SELECT * FROM MyTable
WHERE MyDate >= DATE(NOW())
AND (Category=:cat or '' = :cat)
AND Country IN ($cties)
ORDER BY MyDate ASC");
$stmt->bindValue(':cat', $cat, PDO::PARAM_STR);
$stmt->execute();
I was wondering if this query is secure and if not, what I am doing wrong.
Thanks in advance!
I finally got it (thanks to Your Common Sense):
if(isset($_GET['country'])){
$arr = $_GET['country'];
}
else {
$arr = array('Albania','Andorra','Austria','Belarus','Belgium','Bosnia & Herzegovina','Bulgaria','Croatia','Czech Republic','Denmark','Estonia','Faroe Islands','Finland','France','Germany','Gibraltar','Great Britain','Greece','Hungary','Iceland','Ireland','Isle of Man','Italy','Latvia','Liechtenstein','Lithuania','Luxembourg','Macedonia','Malta','Moldova','Monaco','Montenegro','Netherlands','Norway','Poland','Portugal','Serbia','Romania','San Marino','Slovakia','Slovenia','Spain','Sweden','Switzerland','Ukraine','United Kingdom');
}
if(isset($_GET['category'])){
$cat = $_GET['category'];
}
else{
$cat = " ";
}
// create the Prepared Statement
$in = str_repeat('?,', count($arr) - 1) . '?';
$sql = "SELECT * FROM MyTable WHERE MyDate >= DATE(NOW())
AND Country IN ($in)
AND (Category=? or '' = ?)
ORDER BY MyDate ASC";
$stmt = $con->prepare($sql);
$arr[] = $cat; // adding category to array
$arr[] = $cat; // we need it twice here
// finally - execute
$stmt->execute($arr);
Yeah, Now I see your problem. Well, PDO is not too convenient a library for such a task. So, first of all I'll show you how it can be done with my own library:
$sql = "SELECT * FROM MyTable WHERE MyDate >= CURDATE()
AND (Category=?s or '' = ?s)
AND Country IN (?a)
ORDER BY MyDate ASC"
$data = $db->getAll($sql, $cat, $cat, $_GET['country']);
But I quite realize that you all so inclined to familiar methods. Well, let's elaborate with ugly PDO
First of all, what is the goal? The goal is
to create the query that contains placeholders for all the data. I'll stick to positional placeholders as they are easier to implement.
To create an array with all the variables that have to be bound to placeholders
It seems we need two placeholders for category and some unknown number fro cities. All right, this line will create a string of placeholders:
$in = str_repeat('?,', count($arr) - 1) . '?';
which we are going to insert into query.
// $arr is array with all the vars to bind. at the moment it contains cities only
$arr = $_GET['country'];
// creating string of ?s
$in = str_repeat('?,', count($arr) - 1) . '?';
// building query
$sql = "SELECT * FROM MyTable WHERE MyDate >= DATE(NOW())
AND Country IN ($in)
AND (Category=? or '' = ?)
ORDER BY MyDate ASC";
$stm = $db->prepare($sql);
$arr[] = $_GET['category']; // adding category to array
$arr[] = $_GET['category']; // we need it twice here
// finally - execute
$stm->execute($arr);
$data = $stm->fetchAll();
No, the SQL code could be injected in the $_GET['country'] parameter. You don't escape it anywhere.
See PHP PDO: Can I bind an array to an IN() condition?

Multiple Filters That Work Together Or Independently

I have a page where you can filter users by their profile information.
I had 4 different filters that were "City", "Age Above", "Age Below", and "Gender".
I made it work this way, putting every possible combination as an if statement so that all the filters could work separately or in any combination:
if (isset($ageabove)
&& empty($agebelow)
&& empty($gender)
&& empty($city)
)
{
$sql = mysqli_query($con, "select * from Users1
WHERE age >= 1
");
}
This worked, but it was a lot of combinations and I did fear that this may be an inefficient way to do it.
Now I decided that I need to add 1 more filter, making it a total of 5 filters. This would increase the amount 'if' statements exponentially and I am wondering if there is a better way to accomplish this?
If I haven't been clear enough, please let me know.
Any help much appreciated!
$query = "SELECT stuff FROM table WHERE foo=1 ";
if ($filter1) {
$query .= "AND filter1 = $val";
}
if ($filter2) {
$query .= "AND filter2 = $val";
}
// run query
Would something like that work? (if (isset($filter1) && !empty($filter1))..)
Don't store age in a database. Unless the user specifically goes in and edits it they will be whatever age they signed up as forever. Store their birthdate and calculate age on the fly with:
SELECT DATE_FORMAT(NOW(), '%Y') - DATE_FORMAT(dob, '%Y') - (DATE_FORMAT(NOW(), '00-%m-%d') < DATE_FORMAT(dob, '00-%m-%d')) AS age
I prefer to do this for dynamic conditions in SQL queries:
$query = 'SELECT * FROM table';
$conditions = array();
$parameters = array();
if( isset($_GET['cond1']) ) {
$parameters[] = $_GET['cond1'];
$conditions = 'column1 = ?';
}
if( isset($_GET['cond2']) ) {
$parameters[] = $_GET['cond2'];
$conditions = 'column2 = ?';
}
// etcetera...
if( ! empty(conditions) ) {
$query .= ' WHERE ' . implode(' AND ', $conditions);
$sth = $db->prepare($query);
$rs = $sth->(execute($parameters));
} else {
$sth = $db->prepare($query);
$rs = $sth->(execute());
}
if( ! $rs ) { /* error message */ }
// yadda yadda yadda
Which will build you a query like:
SELECT * FROM TABLE WHERE column1 = ? AND column2 = ? AND ... columnN = ?
as well as placing all of your arguments into the array in the proper order.
I might be a little squiffy on the parameterization for MySQLi, though. I'm a PDO guy.
To make your filters more dynamic, register them in the database, like that:
You can create a model_filters table (in mysql for example):
drop table if exists model_filters;
create table model_filters (
id int not null auto_increment primary key,
name varchar(80) not null,
model varchar(80) not null,
condition varchar(40) not null,
created datetime,
modified datetime,
active boolean not null default 1,
index model_filters (name)
);
So, create some filters, for specific model:
INSERT INTO model_filters VALUES
('Age Above' , 'user' ,'age <= %filter'),
('Age Below' , 'user' ,'age >= %filter'),
('City' , 'user' ,'city ="%filter"'),
('Gender' , 'user' ,'gender = "%filter"');
Then, get the filters based in your model:
SELECT id, name FROM model_filters WHERE model = 'user' AND active = 1
Iterate this values and generate a filters <select>:
<select name="filters" id="filters">
<option value="1">Age Above</option>
<option value="2">Age Below</option>
<option value="3">City</option>
<option value="4">Gender</option>
</select>
<input type="text" name="value" id="value">
And, you get this information, search for selected filter, and then execute your query
<?php
// I'm using PDO
$statment = $pdo->prepare("SELECT * FROM model_filters WHERE id = :id");
$statment->bindValue(':id', $_POST['filters']);
$status = $statment->execute();
$result = null;
if ($status == true) {
$result = $statment->fetchAll(PDO::FETCH_ASSOC);
}
if ($result == null) {
// ERROR!
}
Now, organize the values
/*
$result['condition'] is equal (for example): "age <= %filter"
After replace is = "age <= :filter"
Obs.: replace for whatever you want, for example: ":filtername, :value, :whatever"
*/
$condition = str_replace('%filter', ':filter', $result['condition']);
$statment = $pdo->prepare("SELECT * FROM Users1 WHERE $condition");
$statment->bindValue(':filter', $_POST['value']);
$status = $statment->execute();
$result = null;
if ($status == true) {
$result = $statment->fetchAll(PDO::FETCH_ASSOC);
}
Get $result, sent for your view, and iterate the filtered users.
Done!

Categories