Php resolve blocks of text to make query - php

I am trying to write mysql query for search which is based on the following search.
Search Query string: "(foo and bar) or (blah1 and blah2)"
User will input the query string as mentioned above, it might be a single block or more than one block.
What i want to do is split the string into block of brackets resolve them individually and then combine it with OR or AND statement.
So my end query would look like
Select
*
from
table
where
(field like 'foo%' AND field like 'bar%') OR
(field like 'blah1%' AND field like 'blah2%')
Please help.

There are a couple of issues.
You need to explode out the query string into individual elements, and then explode those out to individual values, then build up the query.
You also need to make the input safe, either by escaping it or using prepared statement.
Using mysqli to use prepared statements something like this:-
$query_string = strtolower("(foo and bar) or (blah1 and blah2)");
$queries_sets = explode(' or ', $query_string);
$query_set = array();
$fields = array();
foreach($queries_sets AS $query_set_key=>$query_set_value)
{
$query_line = explode(' and ', trim($query_set_value, '()'));
if (count($query_line) == 2)
{
$query_set[] = "(field like ? AND field like ?)";
$fields[] = $query_line[0].'%';
$fields[] = $query_line[1].'%';
}
}
$stmt = $conn->prepare("SELECT * FROM table WHERE ".implode(' OR ', $query_set));
call_user_func_array(array($stmt, 'bind_param'), $fields);

Related

Php search Splitting criteria type

I have a php search form with two fields. One for $code another for '$name'.The user uses one or the other, not both.
The submit sends via $_POST.
In the receiving php file I have:
SELECT * FROM list WHERE code = '$code' OR name = '$name' ORDER BY code"
Everything works fine, however I would like that $code is an exact search while $name is wild.
When I try:
SELECT * FROM list WHERE code = '$code' OR name = '%$name%' ORDER BY code
Only $code works while $name gives nothing. I have tried multiple ways. Changing = to LIKE, putting in parentheses etc. But only one way or the other works.
Is there a way I can do this? Or do I have to take another approach?
Thanks
If you only want to accept one or the other, then only add the one you want to test.
Also, when making wild card searches in MySQL, you use LIKE instead of =. We also don't want to add that condition if the value is empty since it would become LIKE '%%', which would match everything.
You should also use parameterized prepared statements instead of injection data directly into your queries.
I've used PDO in my example since it's the easiest database API to use and you didn't mention which you're using. The same can be done with mysqli with some tweaks.
I'm using $pdo as if it contains the PDO instance (database connection) in the below code:
// This will contain the where condition to use
$condition = '';
// This is where we add the values we're matching against
// (this is specifically so we can use prepared statements)
$params = [];
if (!empty($_POST['code'])) {
// We have a value, let's match with code then
$condition = "code = ?";
$params[] = $_POST['code'];
} else if (!empty($_POST['name'])){
// We have a value, let's match with name then
$condition = "name LIKE ?";
// We need to add the wild cards to the value
$params[] = '%' . $_POST['name'] . '%';
}
// Variable to store the results in, if we get any
$results = [];
if ($condition != '') {
// We have a condition, let's prepare the query
$stmt = $pdo->prepare("SELECT * FROM list WHERE " . $condition);
// Let's execute the prepared statement and send in the value:
$stmt->execute($params);
// Get the results as associative arrays
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
The variable $results will now contain the values based on the conditions, or an empty array if no values were passed.
Notice
I haven't tested this exact code IRL, but the logic should be sound.

Build a dynamic SQL statement via $_POST

So this is more about getting your opinion on the best approach for this.
I have what I think is quite an elegant way of building a simple dynamic SQL statement with a straightforward WHERE clause. The WHERE clause can include multiple fields but it is limited as it does not allow for different operators (comparative or logical).
I can build the following with this:
SELECT * from table_name WHERE field_1 = "value_1" AND field_2 = "value_2";
//or I can do
SELECT * from table_name WHERE field_1 = "value_1" OR field_2 = "value_2";
//or I can do
SELECT * from table_name WHERE field_1 <> "value_1" AND field_2 <> "value_2";
I can not build the following:
SELECT * from table_name WHERE field_1 = "value_1" AND field_2 <> "value_2";
//nor can I do
SELECT * from table_name WHERE field_1 = "value_1" AND field_2="value_2" OR field_3 = "value_3
It becomes a real problem when working with numbers and dates when I want to look for records with values between meaning I need to pass the same filed in twice with two separate values.... doesnt it?
SELECT * from table_name WHERE price BETWEEN 10 AND 20;
SELECT * from table_name WHERE date BETWEEN "2016-08-01" AND "2016-08-15";
And not forgetting multiple criteria with "IN" or LIKE statements which this also does not build, i.e.:
SELECT * from table_name WHERE field_1 IN("value_1","value_2, "value_3");
SELECT * from table_name WHERE field_1 LIKE "val%";
Here is what my current code looks like:
// db contains my DB connection
$db = new DB();
$where = 'WHERE';
$criteria = array();
foreach ($_GET as $key => $value) {
$where = $where.' '.$key.'=? AND';
array_push($criteria,$value);
}
if(count($_GET) > 0){
// $sql will look like: SELECT * FROM table_name WHERE field_1 = ? AND field_2 = ?
// $criteria is an array of values to pair with the above prepared statement.
// Will look like: $criteria("value_1", "value_2")
$sql = 'SELECT * FROM mcl_data_gap '.$where;
$results = $db->query($sql,$criteria);
} else {
$sql = 'SELECT * FROM mcl_data_gap';
$results = $db->query($sql);
}
// .... continue on using above SQL statement
In the above code I have used get but my assumption is post would also work.
The only idea I have come up with is to insert more key value pairs that contain the operators required in a coded format that would allow me to then look for these operators and build the statement based on them but I just feel like there is a better way and that is what I am hoping you can help with.
Another option I have just thought of is building the SQL before passing it to the server and just executing that.
Or can I post objects that contain the whole segment of the WHERE statement?
You are using query parameters for the dynamic values (the right side of the equality comparison). This is good.
But you can't use a parameter for the dynamic column names (the left side of a comparison). This is how your code is vulnerable to SQL injection. Prepared statements don't help with that.
The solution is to make sure every column name that comes from your $_GET keys is actually one of the columns in your table. In other words, this is called whitelisting the input.
$mcl_data_gap_columns = ["field_1", "field_2", "field_3"];
You only want to process $_GET parameters that match columns in your list of columns that exist in your table. Anything that isn't in this list should be ignored.
As for predicates that have multiple values, you can access them in PHP by naming the GET parameter with "[]" at the end.
$terms = [];
$parameters = [];
// only look for $_GET keys that match one of the known columns.
// this automatically ignores all other $_GET keys.
foreach ($mc_data_gap_columns as $col) {
// get the single value, or the array of multiple values.
// convert to an array in either case.
if (isset($_GET[$col])) {
$values = (array) $_GET[$col];
$default_op = "=";
} elseif (isset($_GET[$col."[]"])) {
$values = $_GET[$col."[]"];
$default_op = "IN";
} else {
continue;
}
// if your comparison is anything other than equality,
// there should be another request parameter noting that.
if (isset($_GET[$col."_SQLOP"])) {
$op = $_GET[$col."_SQLOP"];
} else {
$op = $default_op;
}
Process only the known operations. If $op is not one of the specific supported operations, ignore it or else throw an error.
switch ($op) {
case "=":
case ">":
case "<":
case ">=":
case "<=":
case "<>":
// all these are simple comparisons of one column to one value
$terms[] = "$col $op ?";
$parameters[] = $values[0];
break;
case "BETWEEN":
// comparisons of one column between two values
if (count($values) != 2) {
error_log("$col BETWEEN: wrong number of arguments: " . count($values));
die("Sorry, there has been an error in your request.");
}
$terms[] = "$col BETWEEN ? AND ?";
$parameters[] = $values[0];
$parameters[] = $values[1];
break;
case "IN":
// comparisons of one column IN a list of any number of values
$placeholders = implode(",", array_fill(1, count($values), "?"));
$terms[] = "$col IN ($placeholders)";
$parameters = array_merge($parameters, $values);
break;
default:
error_log("Unknown operation for $col: $op");
die("Sorry, there has been an error in your request.");
}
}
Then finally after that's done, you'll know that $terms is either an empty array or else the array of search conditions.
if ($terms) {
$sql .= " WHERE " . join(" AND ", $terms);
}
$db->query($sql, $parameters);
I have not tested the above code, but it should illustrate the idea:
Never use $_GET input verbatim in your SQL queries
Always filter input against a fixed list of safe values
Or use switch to test against a fixed set of safe cases
Another option I have just thought of is building the SQL before passing it to the server and just executing that.
No, no, no! This would just be an invitation to get hacked. Never do this!
Your wrong if you think that your HTML page is the only way someone can submit a request to your server. Anyone can form any URL they want, and submit it to your site, even if it contains GET parameters and values you don't expect.

Dynamic MySQL query

I'm trying to create a dynamic sql query that compares my cat column to whatever the user entered in a form. The idea is that I will be able to take a dynamic array of values and then compare them to the cat column. This is what I tried to do:
// Loop to get the array of values from form
$get_arr = $_GET;
foreach ($get_arr as $get) {
$var = "AND cat LIKE $get";
}
// SQL query
$sql = "SELECT * FROM items
WHERE title LIKE 'this'
AND description LIKE 'that'
'%$var%'";
It doesn't work -- $var always show up blank. What would the solution be?
You have several problems.
You're not escaping the input, so you're subject to SQL injection or syntax errors.
You need to put quotes around the LIKE parameter.
You're overwriting $var each time through the loop instead of appending to it.
You're not putting any spaces around the expression.
You're putting % around the whole $var, it should be inside the LIKE parameter.
foreach ($get_arr as $get) {
$get = mysqli_real_escape_string($conn, $get);
$var .= " AND cat like '%$get%'";
}
$sql = "SELECT * FROM items
WHERE title LIKE '%this%'
AND description LIKE '%that%'
%var";

How to combat SQL injection vulnerability when it's hard to use bind variables?

I am used to using parameterised statements but in this statement the number of the parameters can change depending on the number of words in a search string.
So "food" = 1 param, "some food" = 2, "lots of food" = 3 etc etc
I have basically set up a query to allow for searching of a website, to improve functionality i have made it possible to search certain words of columns without it have to be a perfect match.
For example, i concat a club name and its city so;
Name: Mint Club,
City: Leeds
Col: Mint Club Leeds
Searching: 'Mint Leeds' would normally result in the row not being found. But with this modification it works.
With the unknown number of parameters i basically want to know how to secure my statement as i think its still open to sql injection.
Code Below:
$keystring = $mysqli->real_escape_string($_POST["s"]);
$key = strtoupper($keystring);
$key_arr = explode(" ", $key);
$num_key = count($key_arr);
if($num_key >= 2){
$where = '';
foreach($key_arr as $val){
$where .= "(CONCAT(name,' ',city) LIKE '%".$val."%') AND ";
}
$where = substr($where, 0, -4);
$clubWhere = $where;
}
else{
$key = "%".$key."%";
$clubWhere = "(CONCAT(name,' ',city) LIKE '".$key."')";
}
$search_stmt = $mysqli->prepare("SELECT id, name, type AS 'col3', city AS 'col4', 'Club' AS 'table' FROM studentnights_clubs WHERE ".$clubWhere."
UNION
SELECT id, name, description AS 'col3', image AS 'col4', 'Event' AS 'table' FROM studentnights_events WHERE UCASE(name) LIKE ?
UNION
SELECT id, name, '' AS 'col3', '' AS 'col4', 'Genre' AS 'table' FROM studentnights_music WHERE UCASE(name) LIKE ?
ORDER BY
CASE
WHEN name LIKE ? THEN 1
WHEN name LIKE ? THEN 3
ELSE 2
END
LIMIT 10");
$search_stmt->bind_param('ssss', $key, $key, $key, $key);
$search_stmt->execute();
$search_stmt->store_result();
$search_stmt_num = $search_stmt->num_rows;
$search_stmt->bind_result($id, $name, $col3, $col4, $table);
it's a pity you choose raw mysqli for the task - most toilsome API ever existed.
for the fulltext search you should really use either dedicated mysql feature or external search engine like Sphinx Search. It will not only relieve you from this toilsome and inadequate code but it will be actually works with real database, not only with test recordset of hundred rows.
if you want to stick to both inadequate mysql search and mysqli api (as, in my experience, PHP users are especially reluctant for the proper tools) then at least use parameters and then dynamical binding.
Something like this (removing all the useless branching),
$where = '';
$values = array();
foreach(explode(" ", $key) as $val)
{
$values[] = "%$val%";
$where .= "(CONCAT(name,' ',city) LIKE ?) AND ";
}
$where = substr($where, 0, -4);
then add other values to the $values array
$values[] = $key;
$values[] = $key;
$values[] = $key;
$values[] = $key;
and then use dynamical binding as explained here https://stackoverflow.com/a/17874410/285587
Your $val and $key items are user-supplied search parameters, so you need to make sure there's no way for rubbish in them to get through to the query.
One possibility: Prepare two different SQL statements, and use the one with the appropriate number of bind variables.
Another: sanitize the incoming strings before using them to construct SQL statements. You said they're place names, supposedly like 'newcastle-upon-tyne', 'denver', or 'new york'. They are probably not 'new ; drop table super_users;' for example.
So here's how you might sanitize them.
$val = $mysqli->real_escape_string(preg_replace('[^- A-Za-z0-9]', '', $val ));
This line of code will eliminate all characters from $val except alphabetics, numerics, space, and dash. Then it will put escapes into the string to sanitize it for mysqli.
If you preprocess 'new ; drop table super_users;' you'll get 'new drop table superusers', which is safe enough to use in a query, although meaningless.
You could also look for forbidden characters in the user-supplied search terms and refuse to do the search if they were present.
Look, we all know it's much safer to use bind variables, but if you can't you can still sanitize stuff.

prevent sql injection on query with variable (and large) number of columns

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)

Categories