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.
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 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.
I'm trying to take an array and implode it and than run it through a mysql query to search my database for matches. If there are matches, I wanna return the matching values. It keeps returning false and I'm not sure why. I did a vardump and can see the array is there, but doesn't seem to be getting passed to the mysql_query. If I manually put the array into the query it works no problem. Any ideas?
My Array (This comes from my Android App):
$refids = (jdu23764js84, 2746272jsjs7f, 39823874hbsjsk)
PHP script code:
public function searchList($refids) {
$refarray = array($refids);
$comma_separated = implode(',', $refarray);
$result = mysql_query("SELECT `ref_id` FROM `main` WHERE `ref_id` IN
({$comma_separated})");
if ($result == true){
$result = mysql_fetch_array($result);
return $result;
} else {
return false;
}
You've forgotten to quote the individual values inside your $refids, so you're building
... WHERE `ref_id` IN (jdu23764js84, 2746272jsjs7f, ...)
and MySQL is interpreting those as field names. In other words, you're suffering from an SQL injection attack vulnerability, and your utter lack of ANY error handling on the database code is preventing from seeing the errors mysql is trying to tell you about:
$csv = implode("','", $refarray);
^-^-- note the addition of the quotes:
$sql = "SELECT .... `ref_id` IN ('{$csv}')";
^------^--- again, note the quotes
This fixes the problem in the short term. In the long term, you need to read through http://bobby-tables.com and learn what it has to tell you.
I am making a query like this:
$b1 = $_REQUEST['code'].'A'; //letter 'A' is concatenated to $_REQUEST['code']
$a = $_REQUEST['num'];
echo $b1.$a;
$sql = "SELECT '".$b1."' FROM student_record1 WHERE id=".$a;
$result = mysql_query($sql);
if(!$result)
{
echo '<p id="signup">Something went wrong.</p>';
}
else
{
$str = $row[0]
echo $str;
}
Here $b1 and $a are getting values from another page. The 'echo' in the third line is giving a correct result. And I am not getting any error in SQL. Instead, I am not getting any result from the SQL query. I mean echo at the last line.
Don't do this, it breaks your relational model and is unsafe.
Instead of having a table with columns ID, columnA, columnB, columnC, columnD, columnE and having the user select A/B/C/D/E which then picks the column, have a table with three columns ID, TYPE, column and have TYPE be A/B/C/D/E. This also makes it easier to add F/G/H/I afterwards without modifying the table.
Secondly, with the extra column approach you don't have to build your SQL from input values like that. You can use prepared statements, and be safe from SQL Injection. Building SQL from unfiltered strings is wrong, and very dangerous. It will get your site hacked.
If you must use dynamic table/column/database names, you'll have to run them through a whitelist.
The following code will do:
$allowed_column = array('col1', 'col2');
$col = $_POST['col'];
if (in_array($col, $allowed_column)) {
$query = "SELECT `$col` FROM table1 ";
}
See: How to prevent SQL injection with dynamic tablenames?
For more details.
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.