Serverside Datatable search show extra data

Hi I’m working on processing data in datatbale using serverside processing. I’ve managed to show the data in the table but I haven’t fully understood how it works with searches, I’ve included the code in the php file I request, but when I search for something with a where clause in the query it stil shows other values as well, this is my code for request.php

    // storing  request (ie, get/post) global array to a variable  
    $requestData = filter_var_array($_REQUEST); 

    $columns = array( 
    // datatable column index  => database column name
        0 => 'user_first', 
        1 => 'user_last',
        2 => 'user_email',

    );

    // getting total number records without any search
    $sql = "SELECT user_id, user_first, user_last, user_email, user_role";
    $sql .=" FROM users WHERE user_role='partner'";

    $query= mysqli_query($conn, $sql);

    $totalData = mysqli_num_rows($query);

    $totalFiltered = $totalData;  // when there is no search parameter then total number rows = total number filtered rows.

    if( !empty($requestData['search']['value']) ) {
        // if there is a search parameter
        $sql = "SELECT user_first, user_last, user_email";
        $sql.=" FROM users WHERE user_role='partner'";
        $sql.=" AND user_first LIKE '".$requestData['search']['value']."%' ";    // $requestData['search']['value'] contains search parameter
        $sql.=" OR user_last LIKE '".$requestData['search']['value']."%' ";
        $sql.=" OR user_email LIKE '".$requestData['search']['value']."%' ";
        $query=mysqli_query($conn, $sql);
        $totalFiltered = mysqli_num_rows($query); // when there is a search parameter then we have to modify total number filtered rows as per search result without limit in the query 
        $sql.=" ORDER BY ". $columns[$requestData['order'][0]['column']]." ".$requestData['order'][0]['dir']." LIMIT ".$requestData['start']." ,".$requestData['length']." "; // $requestData['order'][0]['column'] contains colmun index, $requestData['order'][0]['dir'] contains order such as asc/desc , $requestData['start'] contains start row number ,$requestData['length'] contains limit length.
        $query=mysqli_query($conn, $sql); // again run query with limit
        
    } else {    
        $sql = "SELECT user_first, user_last, user_email";
        $sql.=" FROM users WHERE user_role='partner'";
        $sql.=" ORDER BY ". $columns[$requestData['order'][0]['column']]."   ".$requestData['order'][0]['dir']."   LIMIT ".$requestData['start']." ,".$requestData['length']."   ";
        $query=mysqli_query($conn, $sql);
        
    }

    $data = array();

    while( $row=mysqli_fetch_array($query) ) {  // preparing an array

        // Query al database per ottenere i gruppi a cui appartengono i condomini
        $query_group_id = mysqli_prepare($conn, "SELECT group_name, group_id FROM user_group_join LEFT OUTER JOIN user_group ON user_group_join . group_join_id = user_group . group_id WHERE user_join_id = ?");
        mysqli_stmt_bind_param($query_group_id, 'i', $row["user_id"]);
        mysqli_stmt_execute($query_group_id);
        mysqli_stmt_store_result($query_group_id);
        mysqli_stmt_bind_result($query_group_id, $group_names, $group_ids);
       
        $response=array();
          
          while (mysqli_stmt_fetch($query_group_id)){
            
            $response[]=$group_names;
            
          }

        $nestedData=array(); 

        $nestedData[] = '<b><a onclick="ViewPartnerDetail('.$row["user_id"].')">'.ucfirst($row["user_first"]).' '.ucfirst($row["user_last"]).'</a></b>';
        $nestedData[] = $row["user_email"];
        $nestedData[] = '<select class="bs-select form-control show-menu-arrow"><option>'.implode("<option>",$response).'</option></select>';
        $nestedData[] = '<button onclick="GetPartnerDetail('.$row["user_id"].')" class="btn btn-sm blue center"> <i class="fa fa-pencil-square-o"></i></button>';
        $nestedData[] = '<button onclick="DeletePartner('.$row["user_id"].')" class="btn btn-sm red"> <i class="fa fa-trash-o"></i></button>';
       
        $data[] = $nestedData;

    }

    $json_data = array(

                "draw" => intval( $requestData['draw'] ),   // for every request/draw by clientside , they send a number as a parameter, when they recieve a response/data they first check the draw number, so we are sending same number in draw. 
                "recordsTotal" => intval( $totalData ),  // total number of records
                "recordsFiltered" => intval( $totalFiltered ), // total number of records after searching, if there is no searching then totalFiltered = totalData
                "data" => $data   // total data array

                );

    echo json_encode($json_data);  // send data as json format

You may be running into a thing with mysqli where you need to consume the results of your first query, before you can use the results of your second query. This applies to both the first query where you run it just to count the matching records but don’t do anything with the data, and with the second query inside your loop.

If the only reason that you run that first one is to get the total number of records, then you should use count in the query, so you don’t get the problem.

$sql = "select count(*) as tdata from users where user_role = 'partner'";
$query = mysqli_query($sql);
$totalData = mysqli_fetch_array($query); 

On the query inside the loop, isn’t it possible to link that data in the initial query with a JOIN?

I suspect there’s a bit missing from this part:

.implode("<option>",$response).

Hi @droopsnoot you are very helpful as usual.
I believe the problem was in the sql query inside the search statement.
It seems to work if I do

$sql = "SELECT user_first, user_last, user_email, user_id ";
$sql.=" FROM users"; $sql.=" WHERE ( user_first LIKE '".$requestData['search']['value']."%' "; 
$sql.=" OR user_last LIKE '".$requestData['search']['value']."%' "; 
$sql.=" OR user_email LIKE '".$requestData['search']['value']."%' )";
 $sql.=" AND user_role='partner'";

Your script works very well with SQL-Injection to let users wipe your database

Hi @chorn thanks for your reply I haven’t implemented prepared statement yet, I’ll definitely work on it, in the mean time does the following code not help to mitigate possible injection? Many thanks

i don’t know what you expect filter_var_array() to do at all when not providing any filtering information as the manual states it - but especially for security reasons you should stop guessing and start testing this.

Hi @chorn yes you are right I didn’t set the filter, i’ve now changed to

$requestData = filter_var_array( $_REQUEST, FILTER_SANITIZE_STRING);

Also I’m not very good with testing for injections and I’ll follow your suggestion to stop guessing and start testing it, could you please give me and example of injection i can test with this code?

I’ll also update this post when i’ve coded the prepared statement so maybe you can give me more suggestions.

many thanks

still filter_var_array is pretty useless, just stop using this for database security.

OWASP covers a wide range of security issues, a multiple of what i can provide alone, so i would recommend to have a look at this

to get a better understanding of the situation and an attackers perspective.

here is a quick summary for a testing example:

Hi @chorn thanks for your answer I’ve tryed to convert mysqli_query to prepared statement but it doesn’t work, I don’t really understand how it works with % this is my code so far

$sql = "SELECT user_first, user_last, user_email, user_id ";
        $sql.=" FROM users";
        $sql.=" WHERE ( user_first LIKE ?% ";
        $sql.=" OR user_last LIKE ?% ";
        $sql.=" OR user_email LIKE ?% )";
        $sql.=" AND user_role='partner'";


        $query= mysqli_stmt_prepare($conn, $sql);
        mysqli_stmt_bind_param($query, 'sss', $requestData['search']['value'], $requestData['search']['value'], $requestData['search']['value']  );
        mysqli_stmt_execute($query);
        mysqli_stmt_store_result($query);
        $totalFiltered = mysqli_stmt_num_rows($query);

It’s been a while but IIRC I had to put the wildcards in the bind not the query. eg.

 ... is, $safe_id, '%' . $safe_search_term . '%' ....

Hi @Mittineague I beleive I’ve managed to sort it out, this is the full code, do you think it is now safe from sql injections?


   // storing  request (ie, get/post) global array to a variable  
    $requestData = filter_var_array($_REQUEST, FILTER_SANITIZE_STRING); 

    $searchString= $requestData['search']['value'].'%';

    $columns = array( 
    // datatable column index  => database column name
        0 => 'user_first', 
        1 => 'user_last',
        2 => 'user_email',
        3 => 'user_id'
    );

    // getting total number records without any search
    $sql = "SELECT user_first, user_last, user_email, user_id";
    $sql .=" FROM users WHERE user_role='partner'";

    $query= mysqli_prepare($conn, $sql);
    mysqli_stmt_execute($query);
    mysqli_stmt_bind_result($query, $user_first, $user_last, $user_email, $user_id);
    mysqli_stmt_store_result($query);
    $totalData = mysqli_stmt_num_rows($query);

    $totalFiltered = $totalData;  // when there is no search parameter then total number rows = total number filtered rows.

    if( !empty($requestData['search']['value']) ) {
        // if there is a search parameter
        $sql = "SELECT user_first, user_last, user_email, user_id ";
        $sql.=" FROM users";
        $sql.=" WHERE ( user_first LIKE ? ";    // $requestData['search']['value'] contains search parameter
        $sql.=" OR user_last LIKE ? ";
        $sql.=" OR user_email LIKE ? )";
        $sql.=" AND user_role='partner'";
        $sql.=" ORDER BY ". $columns[$requestData['order'][0]['column']]." ".$requestData['order'][0]['dir']." LIMIT ".$requestData['start']." ,".$requestData['length']."   ";

        $query= mysqli_prepare($conn, $sql);
        mysqli_stmt_bind_param($query, 'sss',  $searchString,  $searchString,  $searchString);
        mysqli_stmt_execute($query);
        mysqli_stmt_bind_result($query, $user_first, $user_last, $user_email, $user_id);
        mysqli_stmt_store_result($query);
        $totalFiltered = mysqli_stmt_num_rows($query);
   
    } 

    $data = array();

    while( mysqli_stmt_fetch($query) ) {  // preparing an array

        // Query al database per ottenere i gruppi a cui appartengono i condomini
        $query_group_id = mysqli_prepare($conn, "SELECT group_name, group_id FROM user_group_join LEFT OUTER JOIN user_group ON user_group_join . group_join_id = user_group . group_id WHERE user_join_id = ?");
        mysqli_stmt_bind_param($query_group_id, 'i', $user_id);
        mysqli_stmt_execute($query_group_id);
        mysqli_stmt_store_result($query_group_id);
        mysqli_stmt_bind_result($query_group_id, $group_names, $group_ids);
       
        $response=array();
          
          while (mysqli_stmt_fetch($query_group_id)){
            
            $response[]=$group_names;
            
          }

        $nestedData=array(); 

        $nestedData[] = '<b><a onclick="ViewPartnerDetail('.$user_id.')">'.ucfirst($user_first).' '.ucfirst($user_last).'</a></b>';
        $nestedData[] = $user_email;
        $nestedData[] = '<select class="bs-select form-control show-menu-arrow"><option>'.implode("<option>",$response).'</option></select>';
        $nestedData[] = '<button onclick="GetPartnerDetail('.$user_id.')" class="btn btn-sm blue center"> <i class="fa fa-pencil-square-o"></i></button>';
        $nestedData[] = '<button onclick="DeletePartner('.$user_id.')" class="btn btn-sm red"> <i class="fa fa-trash-o"></i></button>';
       
        $data[] = $nestedData;

    }

    $json_data = array(

                "draw" => intval( $requestData['draw'] ),   // for every request/draw by clientside , they send a number as a parameter, when they recieve a response/data they first check the draw number, so we are sending same number in draw. 
                "recordsTotal" => intval( $totalData ),  // total number of records
                "recordsFiltered" => intval( $totalFiltered ), // total number of records after searching, if there is no searching then totalFiltered = totalData
                "data" => $data   // total data array

                );

    echo json_encode($json_data);  // send data as json format

I wish I knew. I am not a someone that tries to exploit vulnerabilities, but I have seen a few examples that opened my eyes to the fact that there is a lot I don’t know. This has made me paranoid so that I probably do a lot more than I need to. If I expect numbers only, I pass the user supplied data through a scrubber, same if I expect alpha characters only. I trim every user supplied value. If I expect a certain range of character count, I check that. If I expect it to be a certain pattern, I regex it.

In other words, I think long and hard about what valid and clean input would be and do everything possible to ensure the input passes. Sometimes I check for blacklisted input, but I prefer a whitelist approach as much as is possible. Others may think using bound parameters does enough, and maybe it does, but as I said, I’m paranoid when it comes to user supplied input.

1 Like

You still have client-data within your query: LIMIT ".$requestData['start'].". So there’s a SQL injection vulnerability. You must totally avoid having client-data in your statements, no exceptions! Just have a look at what @Mittineague said about sanatizing inputs, using whitelists, also explicit type-casting can be used at this point.

Hi, @chorn I think I’ve read. somewhere that parameters are not supported in LIMIT, GROUP, or ORDER BY clauses is it true? Could yu please give me some inputs about compleating this prepared statement? Many thanks

Eek! I just noticed the code has $_REQUEST

IMHO, unless the code absolutely must be able to work with $_GET, $_POST, $_COOKIE inclusively, it is much better to use the specific request method.

Hi @Mittineague yes it works with @_POST just fixed thanks :wink:

1 Like

This does not release you from sanitizing your inputs with validators, type casting and whitelists.

Even if it is not user supplied input, it can be a good idea to make sure the values are OK. Errors can happen and it is often better to ensure data integrity rather than restoring to the most recent backup. Same for if the user input is from only trusted users. mistakes like typos can happen.

I don’t worry so much about SELECTs that use values that come from script because as long as no revealing error messages are displayed it isn’t destructive or dangerous, and for the most part just embarrassing. eg. a select drop down that has no options.

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.