I generate the below query in two ways, but use the same function to insert into the database:
INSERT INTO person VALUES('','john', 'smith','new york', 'NY', '123456');
The below method results in CORRECT inserts, with no extra blank row in the sql database
foreach($_POST as $item)
$statement .= "'$item', ";
$size = count($statement);
$statement = substr($statement, 0, $size-3);
$statement .= ");";
The code below should be generating an identical query to the one above (they echo identically), but when I use it, an extra blank row (with an id) is inserted into the database, after the correct row with data. so two rows are inserted each time.
$mytest = "INSERT INTO person VALUES('','$_POST[name]', '$_POST[address]','$_POST[city]', '$_POST[state]', '$_POST[zip]');";
Because I need to run validations on posted items from the form, and need to do some manipulations before storing it into the database, I need to be able to use the second query method.
I can't understand how the two could be different. I'm using the exact same functions to connect and insert into the database, so the problem can't be there.
below is my insert function for reference:
function do_insertion($query) {
$db = get_db_connection();
if(!($result = mysqli_query($db, $query))) {
#die('SQL ERROR: '. mysqli_error($db));
write_error_page(mysqli_error($db));
} #end if
}
Thank you for any insite/help on this.
Using your $_POST directly in your query is opening you up to a lot of bad things, it's just bad practice. You should at least do something to clean your data before going to your database.
The $_POST variable often times can contain additional values depending on the browser, form submit. Have you tried doing a null/empty check in your foreach?
!~ Pseudo Code DO NOT USE IN PRODUCTION ~!
foreach($_POST as $item)
{
if(isset($item) && $item != "")
{
$statement .= "'$item', ";
$size = count($statement);
$statement = substr($statement, 0, $size-3);
$statement .= ");";
}
}
Please read #tadman's comment about using bind_param and protecting yourself against SQL injection. For the sake of answering your question it's likely your $_POST contains empty data that is being put into your query and resulting in the added row.
as #yycdev stated, you are in risk of SQL injection. Start by reading this and rewrite your code by proper use of protecting your database. SQL injection is not fun and will produce many bugs.
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.
I have successfully gotten queries to execute and print in PDO, but I'm doing something wrong here. The important part of the code for this question is in the last couple blocks of code; I'm including the first portion just for clarity.
This code connects to an HTML form with multiple input fields. The PHP constructs a query by appending the data from each field with ANDs in the WHERE statement.
This is what throws me: I echo the $query variable, and I can see that the query is formed properly, but when I then try to print the query results, no results are printed.
I wrestled with using prepared statements here, and decided to try getting the code to work first without them after failing to construct a prepared statement with varying numbers of parameters. I did try, with the help of this post: LIKE query using multiple keywords from search field using PDO prepared statement
So, setting aside prepared statements for the moment, can anyone tell me what I'm doing wrong here? Any help would be greatly appreciated.
<?php
if(isset($_POST['submit'])) {
// define the list of fields
$fields = array('titleSearch', 'keywordSearch', 'fullSearch', 'fromYear', 'toYear',
'fromSeconds', 'toSeconds', 'withSound', 'withColor');
$conditions = array();
// loop through the defined fields
foreach($fields as $field){
// if the field is set and not empty
if(isset($_POST[$field]) && $_POST[$field] != '') {
// create a new condition, using a prepared statement
$conditions[] = "$field LIKE CONCAT ('%', $_POST[$field], '%')";
}
}
// build the query
$query = "SELECT keyframeurl, videoid, title, creationyear, sound, color,
duration, genre FROM openvideo ";
// if there are conditions defined, append them to the query
if(count($conditions) > 0) {
$query .= "WHERE " . implode(' AND ', $conditions);
}
//confirm that query formed correctly
echo $query;
//print query results
foreach ($dbh->query($query) as $row){
print $row['videoid'].' - '.$row['title'].'<br />';
}
}
?>
Instead of posting your query you have to run it.
That's the only way to fix the problem
a Stack Overflow passer-by do not have a database server in their head to run your query.
a Stack Overflow passer-by do not have your particular database server in their head to run your query.
So, you are the only one who can run your query against your database and ask it what's going wrong.
Turn on error reporting. Make sure sure you can see errors occurred. Try to add intentional error and see if it works.
Double-check your database data if it really contains desired values.
Double-check your input data, if it really match database values.
Run your assembled query against database in console or phpadmin.
Dig to some certain problem. Do not just sit and wait. Asking a question "I have a code it doesnt work" makes very little sense. Code have to be run, not stared into.
$conditions[] = "$field LIKE CONCAT ('%', $_POST[$field], '%')";
is the culprit: sending "something" for the title ends up in something like
WHERE titleSearch LIKE CONCAT('%', something, '%')
but you want
WHERE titleSearch LIKE CONCAT('%', 'something', '%')
with more quotes.
Be sure not to roll this out into production though, as you might end up with somebody posting "xxx') OR 1=1; --" just for the perofrmance fun, or even worse, depedning on their mood.
You've forgotten quotes around the $_POST values that you're directly inserting into your queries:
$conditions[] = "$field LIKE CONCAT ('%', '$_POST[$field]', '%')";
^-- ^--
so while this will fix your immediate problem, you'll still be wide open to sql injection attacks.
You don't even need the CONCAT built-in function, you can model the whole string as $conditions[] = "{$field} LIKE '%{$_POST[$field]}%'". But you should use prepared statements if you don't want to face serious SQL injection attacks in the short-term future.
Why don't you try something like this? (using PDO as an example):
if ($pdo = new \PDO("mysql:host=localhost;dbname=testdb;charset=utf8", "user", "password")) {
$fields = ["titleSearch","keywordSearch","fullSearch","fromYear","toYear","fromSeconds","toSeconds","withSound","withColor"];
$parameters = array_map(function ($input) { return filter_var($input, FILTER_SANITIZE_STRING); }, $fields)
$conditions = array_map(function ($input) { return (!empty($_POST[$input]) ? "{$input} LIKE ?" : null); }, $fields);
$query = "SELECT `keyframeurl`,`videoid`,`title`,`creationyear`,`sound`,`color`,`duration`,`genre` FROM `openvideo`" . (sizeof($conditions) > 0 ? " " . implode(" AND ", $conditions) : null);
if ($statement = $pdo->prepare($query, [\PDO::ATTR_CURSOR => \PDO::CURSOR_FWDONLY])) {
if ($statement->execute((!empty($parameters) ? $parameters : null))) {
$result = $statement->fetchAll(\PDO::FETCH_ASSOC);
}
}
}
Haven't tested it (just coming to my mind right now), but it should set up PDO, prepare a statement based on the conditions you seem to look for, add the parameters in the execute() method (pre-filtered, although there's FAR better filtering techniques) and return all results associated with your query.
Even if you decide not to use this, give it a thought at least... it's a good starting point on PDO and, of course, get a nice tutorial on GET/POST variable filtering (or use a 3rd-party tool like HTML Purifier, for that matter).
Hope that helps ;)
I do not think that this has been posted before - as this is a very specific problem.
I have a script that generates a "create table" script with a custom number of columns with custom types and names.
Here is a sample that should give you enough to work from -
$cols = array();
$count = 1;
$numcols = $_POST['cols'];
while ($numcols > 0) {
$cols[] = mysql_real_escape_string($_POST[$count."_name"])." ".mysql_real_escape_string($_POST[$count."_type"]);
$count ++;
$numcols --;
}
$allcols = null;
$newcounter = $_POST['cols'];
foreach ($cols as $col) {
if ($newcounter > 1)
$allcols = $allcols.$col.",\n";
else
$allcols = $allcols.$col."\n";
$newcounter --;
};
$fullname = $_SESSION['user_id']."_".mysql_real_escape_string($_POST['name']);
$dbname = mysql_real_escape_string($_POST['name']);
$query = "CREATE TABLE ".$fullname." (\n".$allcols." )";
mysql_query($query);
echo create_table($query, $fullname, $dbname, $actualcols);
But for some reason, when I run this query, it returns a syntax error in MySQL. This is probably to do with line breaks, but I can't figure it out. HELP!
You have multiple SQL-injection holes
mysql_real_escape_string() only works for values, not for anything else.
Also you are using it wrong, you need to quote your values aka parameters in single quotes.
$normal_query = "SELECT col1 FROM table1 WHERE col2 = '$escaped_var' ";
If you don't mysql_real_escape_string() will not work and you will get syntax errors as a bonus.
In a CREATE statement there are no parameters, so escaping makes no sense and serves no purpose.
You need to whitelist your column names because this code does absolutely nothing to protect you.
Coding horror
$dbname = mysql_real_escape_string($_POST['name']); //unsafe
see this question for answers:
How to prevent SQL injection with dynamic tablenames?
Never use \n in a query
Use separate the elements using spaces. MySQL is perfectly happy to accept your query as one long string.
If you want to pretty-print your query, use two spaces in place of \n and replace a double space by a linebreak in the code that displays the query on the screen.
More SQL-injection
$SESSION['user_id'] is not secure, you suggest you convert that into an integer and then feed it into the query. Because you cannot check it against a whitelist and escaping tablenames is pointless.
$safesession_id = intval($SESSION['user_id']);
Surround all table and column names in backticks `
This is not needed for handwritten code, but for autogenerated code it is essential.
Example:
CREATE TABLE `table_18993` (`id` INTEGER .....
Learn from the master
You can generate the create statement of a table in MySQL using the following MySQL query:
SHOW CREATE TABLE tblname;
Your code needs to replicate the output of this statement exactly.
$url = mysql_real_escape_string($_POST['url']);
$shoutcast_url = mysql_real_escape_string($_POST['shoutcast_url']);
$site_name = mysql_real_escape_string($_POST['site_name']);
$site_subtitle = mysql_real_escape_string($_POST['site_subtitle']);
$email_suffix = mysql_real_escape_string($_POST['email_suffix']);
$logo_name = mysql_real_escape_string($_POST['logo_name']);
$twitter_username = mysql_real_escape_string($_POST['twitter_username']);
with all those options in a form, they are pre-filled in (by the database), however users can choose to change them, which updates the original database. Would it be better for me to update all the columns despite the chance that some of the rows have not been updated, or just do an if ($original_db_entry = $possible_new_entry) on each (which would be a query in itself)?
Thanks
I'd say it doesn't really matter either way - the size of the query you send to the server is hardly relevant here, and there is no "last updated" information for columns that would be updated unjustly, so...
By the way, what I like to do when working with such loads of data is create a temporary array.
$fields = array("url", "shoutcast_url", "site_name", "site_subtitle" , ....);
foreach ($fields as $field)
$$field = mysql_real_escape_string($_POST[$field]);
the only thing to be aware of here is that you have to be careful not to put variable names into $fields that would overwrite existing variables.
Update: Col. Shrapnel makes the correct and valid point that using variable variables is not a good practice. While I think it is perfectly acceptable to use variable variables within the scope of a function, it is indeed better not use them at all. The better way to sanitize all incoming fields and have them in a usable form would be:
$sanitized_data = array();
$fields = array("url", "shoutcast_url", "site_name", "site_subtitle" , ....);
foreach ($fields as $field)
$sanizited_data[$field] = mysql_real_escape_string($_POST[$field]);
this will leave you with an array you can work with:
$sanitized_data["url"] = ....
$sanitized_data["shoutcast_url"] = ....
Just run a single query that updates all columns:
UPDATE table SET col1='a', col2='b', col3='c' WHERE id = '5'
I would recommend that you execute the UPDATE with all column values. It'd be less costly than trying to confirm that the value is different than what's currently in the database. And that confirmation would be irrelevant anyway, because the values in the database could change instantly after you check them if someone else updates them.
If you issue an UPDATE against MySQL and the values are identical to values already in the database, the UPDATE will be a no-op. That is, MySQL reports zero rows affected.
MySQL knows not to do unnecessary work during an UPDATE.
If only one column changes, MySQL does need to do work. It only changes the columns that are different, but it still creates a new row version (assuming you're using InnoDB).
And of course there's some small amount of work necessary to actually send the UPDATE statement to the MySQL server so it can compare against the existing row. But typically this takes only hundredths of a millisecond on a modern server.
Yes, it's ok to update every field.
A simple function to produce SET statement:
function dbSet($fields) {
$set='';
foreach ($fields as $field) {
if (isset($_POST[$field])) {
$set.="`$field`='".mysql_real_escape_string($_POST[$field])."', ";
}
}
return substr($set, 0, -2);
}
and usage:
$fields = explode(" ","name surname lastname address zip fax phone");
$query = "UPDATE $table SET ".dbSet($fields)." WHERE id=$id";
In PHP, I want to insert into a database using data contained in a associative array of field/value pairs.
Example:
$_fields = array('field1'=>'value1','field2'=>'value2','field3'=>'value3');
The resulting SQL insert should look as follows:
INSERT INTO table (field1,field2,field3) VALUES ('value1','value2','value3');
I have come up with the following PHP one-liner:
mysql_query("INSERT INTO table (".implode(',',array_keys($_fields)).") VALUES (".implode(',',array_values($_fields)).")");
It separates the keys and values of the the associative array and implodes to generate a comma-separated string . The problem is that it does not escape or quote the values that were inserted into the database. To illustrate the danger, Imagine if $_fields contained the following:
$_fields = array('field1'=>"naustyvalue); drop table members; --");
The following SQL would be generated:
INSERT INTO table (field1) VALUES (naustyvalue); drop table members; --;
Luckily, multiple queries are not supported, nevertheless quoting and escaping are essential to prevent SQL injection vulnerabilities.
How do you write your PHP Mysql Inserts?
Note: PDO or mysqli prepared queries aren't currently an option for me because the codebase already uses mysql extensively - a change is planned but it'd take alot of resources to convert?
The only thing i would change would be to use sprintf for readability purposes
$sql = sprintf(
'INSERT INTO table (%s) VALUES ("%s")',
implode(',',array_keys($_fields)),
implode('","',array_values($_fields))
);
mysql_query($sql);
and make sure the values are escaped.
Nothing wrong with that. I do the same.
But make sure you mysql_escape() and quote the values you stick in the query, otherwise you're looking at SQL injection vulnerability.
Alternately, you could use parametrized queries, in which case you can practically pass the array in itself, instead of building a query string.
The best practice is either to use an ORM (Doctrine 2.0), an ActiveRecord implementation (Doctrine 1.0, RedBean), or a TableGateway pattern implementation (Zend_Db_Table, Propel). These tools will make your life a lot easier, and handle a lot of the heavy lifting for you, and can help protect you from SQL injections.
Other than that, there's nothing inherently wrong with what you're doing, you just might want to abstract it away into a class or a function, so that you can repeat the functionality in different places.
Using the sprintf trick mentioned by Galen in a previous answer, I have come up with the following code:
$escapedfieldValues = array_map(create_function('$e', 'return mysql_real_escape_string(((get_magic_quotes_gpc()) ? stripslashes($e) : $e));'), array_values($_fields));
$sql = sprintf('INSERT INTO table (%s) VALUES ("%s")', implode(',',array_keys($_fields)), implode('"," ',$escapedfieldValues));
mysql_query($sql);
It generates a escaped and quoted insert. It also copes independent of whether magic_quotes_gpc is on or off. The code could be nicer if I used new PHP v5.3.0 anonymous functions but I need it to run on older PHP installations.
This code is a bit longer that the original (and slower) but it is more secure.
I use this to retrieve the VALUES part of the INSERT.
But it might be an absurd way to do things. Comments/suggestions are welcome.
function arrayToSqlValues($array)
{
$sql = "";
foreach($array as $val)
{
//adding value
if($val === NULL)
$sql .= "NULL";
else
/*
useless piece of code see comments
if($val === FALSE)
$sql .= "FALSE";
else
*/
$sql .= "'" . addslashes($val) . "'";
$sql .= ", ";
};
return "VALUES(" . rtrim($sql, " ,") . ")";
}
There is a problem with NULL (in the accepted answer) values being converted to empty string "". So this is fix, NULL becomes NULL without quotes:
function implode_sql_values($vals)
{
$s = '';
foreach ($vals as $v)
$s .= ','.(($v===NULL)?'NULL':'"'.mysql_real_escape_string($v).'"');
return substr($s, 1);
}
Usage:
implode_sql_values(array_values( array('id'=>1, 'nick'=>'bla', 'fbid'=>NULL) ));
// =='"1","bla",NULL'
If you want to enhance your approach and add the possibility for input validation and sanitation, you might want to do this:
function insertarray($table, $arr){
foreach($arr as $k => $v){
$col[] = sanitize($k);
$val[] = "'".sanitize($v)."'";
}
query('INSERT INTO '.sanitize($table).' ('.implode(', ', $col).') VALUES ('.implode(', ', $val).')' );
}