So I'm having an odd problem... We use parameterized queries to prevent SQL Injection in our code but I'm having trouble with some of that behavior and while I've found an ugly way around it, my work around kind of defeats the purpose of the parameterization.
Suppose I'm making this query:
$db = new csmysqli('database',$host,$user,$pass);
$value = x;
$stmt = "INSERT INTO table SET value='%s'";
$result = $db->prepare($stmt, $value);
echo $result;
Now here's the problem... if x is a string, or an int we get this for result:
INSERT INTO table SET value='123';
No problem... however, if x is null:
INSERT INTO table SET value='NULL'; <--- the single quotes there cause a problem.... Ok so I try this to get around it:
$value = "'x'"; // Notice the quotes now go around x
$stmt = "INSERT INTO table SET value=%s";
$result = $db->prepare($stmt, $value);
echo $result;
And we get this if x is an int or string:
INSERT INTO table SET value=\'x\';
And the null now works:
INSERT INTO table SET value=NULL;
So the question is:
How can I get both normal data and NULL data to correctly populate with parameterization ?
EDIT:
I should have mentioned I'm using a special mysqli_helper script:
class csmysqli extends mysqli
{
public function __construct($dbname = '', $host,$user,$pass)
{
parent::__construct($host, $user, $pass, $dbname);
}
public function query($query)
{
$numParams = func_num_args();
$params = func_get_args();
//merge in parameters only if needed
if ($numParams > 1) {
for ($i = 1; $i < $numParams; $i++) {
$params[$i] = parent::real_escape_string($params[$i]);
}
$query = call_user_func_array('sprintf', $params);
}
return parent::query($query, MYSQLI_STORE_RESULT);
}
public function prepare($query)
{
$numParams = func_num_args();
$params = func_get_args();
//merge in parameters only if needed
if ($numParams > 1) {
for ($i = 1; $i < $numParams; $i++) {
$params[$i] = parent::real_escape_string($params[$i]);
}
$query = call_user_func_array('sprintf', $params);
}
return $query;
}
}
With native mysqli parameterization there is no problem to get both normal data and NULL data to correctly populate.
With your home-brewed parameterization you have to check the parameter type and act accordingly.
Related
I want to pass some parameters in a method which i would use to insert data into my database. I have over 50 fields in the table i want to insert into. The challenge is that, some of this parameters are optional and so, explicitly writing the parameters and table fields would be very hectic.
I thought of public function addAd(...param){} however, i won't have control over the optional params.
For the table fields, i thought of using SELECT to get the fields then pass it as array into addTable as fields. "INSERT INTO adTable ($fields) VALUES (?,...)"
Is there any way i can solve this problem?
UPDATE
private $col_names;
private $msg;
private function getAdsFields(){
$sql = "SHOW COLUMNS FROM mall";
$stmt = $this->db->run($sql);
while ($row = $stmt->fetch()) {
if ($row['Field'] != 'defaultColID') {
array_push($this->col_names, $row['Field']);
}
}
}
public function createAd(...$params){
$this->getAdsFields();
//gets all the fields returned from getAdsFields then convert them to string
$fields = implode(',', $this->col_names);
if (empty($params)) {
$this->message("empty", "All field must be filled");
} else{
// gets the total number of parameters passed
$num = (count($params));
// loops through the numbers and pushes '?' in to the each loop;
$bind_array = [];
for ($i=0; $i < $num; $i++) {
array_push($bind_array, '?');
}
// coverts all the'?' in the array into strings;
$bind = implode(',',$bind_array);
$sql = "INSERT INTO mall ($fields) VALUES ($bind)";
$stmt = $this->db->run($sql, $params);
if ($stmt->rowCount() > 0) {
$this->message("success", "Category added successfully");
} else {
$this->message("failed", "Something Went Wrong");
}
}
return $this->msg;
}
This actually worked, however, the only set back is that i cannot specify optional parameters.
Ok, so I am having a lot of trouble with Prepared statements. I've done hours of research and still can't seem to fully understand everything...
I really feel like I need to understand Prepared statements because I was just about to release a few new free APIs on my website (which require API Key to execute API) but I recently realized how insecure everything is.... I can simply use SQL injection to bypass API Key check, e.g. 'OR'1'='1
Here is how I validate API Key:
$apikey = $_GET['key'];
$sql = "SELECT * FROM `table` WHERE `key` = '$apikey'";
$query = mysqli_query($con, $sql);
if($query)
{
$fetchrow = mysqli_fetch_row($query);
if(isset($fetchrow[0]))
{
echo "API Key is valid!";
}
else
{
echo "API KEY is invalid";
}
}
And like mentioned above this can easily be bypassed by executing my API like this
http://website.com/api.php?key='OR'1'='1
This really scared me at first, but then I did some research and learned a good way to prevent any form of SQL injection is to use prepared statement, so I did a lot of research and it just seems quite complicated to me :/
So I guess my question is, how can I take my above code, and make it function the same way using prepared statements?
Probably everything you need:
class Database {
private static $mysqli;
Connect to the DB:
public static function connect(){
if (isset(self::$mysqli)){
return self::$mysqli;
}
self::$mysqli = new mysqli("DB_HOST", "DB_USER", "DB_PASS", "DB_NAME");
if (mysqli_connect_errno()) {
/*Log error here, return 500 code (db connection error) or something... Details in $mysqli->error*/
}
self::$mysqli->query("SET NAMES utf8");
return self::$mysqli;
}
Execute statement and get results:
public static function execute($stmt){
$stmt->execute();
if ($mysqli->error) {
/*Log it or throw 500 code (sql error)*/
}
return self::getResults($stmt);
}
Bind results to the pure array:
private static function getResults($stmt){
$stmt->store_result();
$meta = $stmt->result_metadata();
if (is_object($meta)){
$variables = array();
$data = array();
while($field = $meta->fetch_field()) {
$variables[] = &$data[$field->name];
}
call_user_func_array(array($stmt, "bind_result"), $variables);
$i = 0;
while($stmt->fetch()) {
$array[$i] = array();
foreach($data as $k=>$v)
$array[$i][$k] = $v;
$i++;
}
$stmt->close();
return $array;
} else {
return $meta;
}
}
Class end :)
}
Example of usage:
public function getSomething($something, $somethingOther){
$mysqli = Database::connect();
$stmt = $mysqli->prepare("SELECT * FROM table WHERE something = ? AND somethingOther = ?");
$stmt->bind_param("si", $something, $somethingOther); // s means string, i means number
$resultsArray = Database::execute($stmt);
$someData = $resultsArray[0]["someColumn"];
}
Resolving your problem:
public function isKeyValid($key){
$mysqli = Database::connect();
$stmt = $mysqli->prepare("SELECT * FROM table WHERE key = ? LIMIT 1");
$stmt->bind_param("s", $key);
$results = Database::execute($stmt);
return count($results > 0);
}
PHP automatically closes DB connection so no worries about it.
$sql = "SELECT * FROM `table` WHERE `key` = ?";
if(stmt = $mysqli->prepare($sql)) {
$stmt->bind_param("i", $apikey);
$stmt->execute();
$stmt->bind_result($res);
$stmt->fetch();
$stmt->close();
}
See more - http://php.net/manual/en/mysqli.prepare.php
I am creating a random user ID, but I would like to check if the ID already has been used (very unlikely but the chances are there), but somehow this doesn't work. When I look in the database, there is no random character string in the account_id field. Do I call the functions in a wrong way?
function genRandomString() {
$length = 40;
$characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
for ($p = 0; $p < $length; $p++) {
$string .= $characters[mt_rand(0, strlen($characters))];
}
return $string;
}
function createID() {
$cl_id = 'h_u_'.genRandomString();
}
createID();
$sql_query="SELECT * FROM accounts WHERE account_id = :cl_id";
$statement = $conn->prepare($sql_query);
$statement->bindParam(':cl_id', $cl_id, PDO::PARAM_STR);
if ($statement->execute() && $row = $statement->fetch())
{
createID();
}
$conn->exec("INSERT INTO accounts SET
account_id='$cl_id' ,
name='$_POST[name]' ,
email='$_POST[email]' ");
$cl_id is a local variable in createID() function , you need to return your value to your global code ...
function createID() {
return $cl_id = 'h_u_'.genRandomString();
}
you need to check $id in the main code
$id = createID();
$sql_query="SELECT * FROM accounts WHERE account_id = '".$cl_id."'";
$statement = $conn->prepare($sql_query);
1 . You missed to return $c_id in createID(). Change it to:
function createID() {
return 'h_u_'.genRandomString();
}
$cl_id = createID();
2 . You could use good old uniqid() instead of your custom genRandomString().
This would lead to something simpler like:
function createID() {
return 'h_u_'.uniqid();
}
$cl_id = createID();
3 . You'll have to change the if in the database related code to a loop (have a look at my example below)
4 . Your insert query uses unverified $_POST vars. This is highly prone to SQL Injections. If your Database library supports server side prepared statements you should use them and you can feel secure because data is being kept separate from the query syntax. If you are using PHP with MySQL this is the case.
If you are not using server side prepared statements you should escape any $_POST data used in the query by using mysql_real_escape_string() or something like this. In the following example I'm assuming that you are using PHP with MySQL and thatswhy I use a prepared statement.
Taking all this in account may result in a finished script like this:
$sql_query="SELECT * FROM accounts WHERE account_id = :cl_id";
$statement = $conn->prepare($sql_query);
$maxtries = 3; // how many tries to generate a unique id?
for($i = 0; $i < $maxtries; $i++) {
$cl_id = uniqid(); // create a 'unique' id
$statement->bindParam(':cl_id', $cl_id, PDO::PARAM_STR);
if (!$statement->execute()) {
die('db error');
}
$row = $statement->fetch();
if($row) {
continue;
}
break;
}
// if a unique id couldn't get generated even
// after maxtries, then pigs can fly too :)
if($i === $maxtries) {
die('maximum number of tries reached. pigs can fly!');
}
// You should use a prepared statement for the insert to prevent from
// SQL injections as you pass $_POST vars to the query. You should further
// consider to validate email address and the name!
$name = $_POST['name'];
$email = $_POST['email'];
$insert_query = '
INSERT INTO accounts SET
account_id = :account_id,
name = :name,
email = :email';
$insert_statement = $conn->prepare($insert_query);
$insert_statement->bindParam(':account_id', $cl_id, PDO::PARAM_STR);
$insert_statement->bindParam(':name', $name, PDO::PARAM_STR);
$insert_statement->bindParam(':account_id', $email, PDO::PARAM_STR);
if (!$insert_statement->execute()) {
die('db error');
}
I've got a database query function that works well -- except that I'm running into what's apparently a known issue with mysqli prepared statements and longtext fields. What happens is that the longtext field always comes up empty even though running the query through phpMyAdmin works fine. According to http://www.workinginboxershorts.com/php-mysqli-returns-empty-variables-from-longtext-column, switching the datatype to text solves the problem. However, in my case I'd really prefer to leave the field as longtext as I can foresee times when that extra space would be valuable.
I'm using parameterized queries, which evidently is the problem. Here's my function:
// Bind results to an array
// $stmt = sql query, $out = array to be returned
function stmt_bind_assoc (&$stmt, &$out) {
$data = mysqli_stmt_result_metadata($stmt);
$fields = array();
$out = array();
$fields[0] = $stmt;
$count = 1;
while($field = mysqli_fetch_field($data)) {
$fields[$count] = &$out[$field->name];
$count++;
}
call_user_func_array('mysqli_stmt_bind_result', $fields);
}
// DB Query
// $query = SQL query, $params = array of parameters, $rs = whether or not a resultset is expected, $newid = whether or not to retrieve the new ID value;
// $onedimensionkey = key required to convert array into simple one dimensional array
function db_query($query, $params, $rs = true, $newid = false, $onedimensionkey = false) {
$link = mysqli_connect(DB_SERVER, DB_USER, DB_PASS, DB_NAME);
if (!$link) {
print 'Error connecting to MySQL Server. Errorcode: ' . mysqli_connect_error();
exit;
}
// Prepare the query and split the parameters array into bound values
if ($sql_stmt = mysqli_prepare($link, $query)) {
if ($params) {
$types = '';
$new_params = array();
$params_ref = array();
// Split the params array into types string and parameters sub-array
foreach ($params as $param) {
$types .= $param['type'];
$new_params[] = $param['value'];
}
// Cycle the new parameters array to make it an array by reference
foreach ($new_params as $key => $parameter) {
$params_ref[] = &$new_params[$key];
}
call_user_func_array('mysqli_stmt_bind_param', array_merge(array($sql_stmt, $types), $params_ref));
}
}
else {
print 'Error: ' . mysqli_error($link);
exit();
}
// Execute the query
mysqli_stmt_execute($sql_stmt);
// If there are results to retrive, do so
if ($rs) {
$results = array();
$rows = array();
$row = array();
stmt_bind_assoc($sql_stmt, $results);
while (mysqli_stmt_fetch($sql_stmt)) {
foreach ($results as $key => $value) {
$row[$key] = $value;
}
$rows[] = $row;
}
if ($onedimensionkey) {
$i = 0;
foreach ($rows as $row) {
$simplearray[$i] = $row[$onedimensionkey];
$i++;
}
return $simplearray;
}
else {
return $rows;
}
}
// If there are no results but we need the new ID, return it
elseif ($newid) {
return mysqli_insert_id($link);
}
// Close objects
mysqli_stmt_close($sql_stmt);
mysqli_close($link);
}
According to the link that I posted there is a workaround involving the order in which things are done, but either I'm handling my query in a completely different manner than the example or I'm simply not understanding something important.
Thanks to anyone who can help!
EDIT: Thanks to Corina's answer, I've solved this -- for anyone else who runs into the problem, you will simply need to add the following after the mysql_stmt_execute command:
// Execute the query
mysqli_stmt_execute($sql_stmt);
// Store results
mysqli_stmt_store_result($sql_stmt);
I managed to solve the same issue by calling mysqli_stmt_store_result before binding the data.
Someone had the same problem and shared the answer on the php.net website:
Apparently, if you have longtext present, you HAVE to call
store_result before using bind_result.
http://bugs.php.net/bug.php?id=47928
I am using a function in php for all select queries so that i can dynamically retrieve data from my database ..... I just wanted to know that is my code secure and efficient or if their is a better way to do this, if so please point me to the right direction...thanks
class mysql {
private $conn;
function __construct(){
$this->conn= new mysqli(DB_SERVER, DB_USER, DB_PASSWORD, DB_NAME);
if( mysqli_connect_errno() )
{
trigger_error('Error connecting to host. '.$this->connections[$connection_id]->error, E_USER_ERROR);
}
}
function extracting_data($table, $fields,$condition,$order,$limit){
$query="SELECT ".$fields."
FROM ".$table."
WHERE id =".$this->sql_quote($condition)."
ORDER BY ".$order."
LIMIT ".$limit." ";
//echo $query;
if($stmt = $this->conn->prepare($query)) {
$stmt->execute();
$row = array_pad(array(), $stmt->field_count, '');
$params = array();
foreach($row as $k=>$v) {
$params[] = &$row[$k];
}
call_user_func_array(array($stmt,'bind_result'),$params);
$result = array();
while($stmt->fetch()) {
foreach ($row as $b=>$elem) {
$vals[$b]=$row[$b];
}
$result[]=$vals;
}
$stmt->close();
return $result;
}
}
function sql_quote( $value )
{
if( get_magic_quotes_gpc() )
{
$value = stripslashes( $value );
}
//check if this function exists
if( function_exists( "mysql_real_escape_string" ) )
{
$value = mysql_real_escape_string( $value );
}
//for PHP version < 4.3.0 use addslashes
else
{
$value = addslashes( $value );
}
return $value;
}
}
Now to call the function I am using ::>
$connection=New mysql();
$extract=$connection->extracting_data("tablename","id,name,points","$_GET['id']","date desc","0,10");
The function returns a multi-dimensional array in $result and stores it in $extract ,depending on the data I want to extract..
Any improvements or other suggestions would be appreciated ...
Instead of binding the results and having to do loads of looping, you could simply use mysqli::query() and mysqli_result::fetch_all().
if($stmt = $this->conn->query($query)) {
$result = $stmt->fetch_all(MYSQLI_ASSOC);
$stmt->close();
return $result;
}
You would be better off binding your input variables rather than building up an SQL string containing them, but that may not be feasible using your current approach.
Edit
Sorry, I was an idiot and didn't notice that fetch_all() is only in PHP >= 5.3. You can still do this though which is simpler:
if($stmt = $this->conn->query($query)) {
$result = array();
while ($row = $stmt->fetch_assoc()) {
$result[] = $row;
}
$stmt->close();
return $result;
}
You should watch where the parameters for your function come from. If they come from an unreliable source, then it's very insecure.
If someone passes something like 1 ; DROP TABLE tablename ; SELECT * FROM dual WHERE 1 in the $condition parameter, you'll get the Little Bobby Tables scenario.
Your query will look like the following:
SELECT id, name, points
FROM tablename
WHERE id
ORDER BY
DATE DESC
LIMIT 0, 10
The id here will be casted to BOOLEAN, and the query will select all ids except 0 and NULL.
Is it really what you want?
You probably want to change your $condition to 'id = $id' or something like that.
Do you really need this level of abstraction: generating queries from uknown tables with unknown fields but with predefined SELECT / FROM / ORDER BY / LIMIT stucture?