0

I've been trying to put together functions in a more secure way that keeps us safe from injection or manipulating inserts by calling different columns to be updated. In your opinion, is this function safe at all, and if not what would you suggest is a better way to do it, and why.

This function is called when a user updates their profile, or specific parts of their profile, as you can see I've made an array with items which is all they can update in that table. Also, the user_id I am getting is from the secure encrypted JSON token that's attached to their session, they are not sending that. Thanks for your time.

function updateProfile( $vars, $user_id ) {
    $db = new Database();
    $update_string = '';
    $varsCount = count($vars);
    $end = ',';
    $start = 1;
    $safeArray = array( "gradYear", "emailAddress", "token", "iosToken", "country", 
"birthYear", "userDescription" );
    foreach($vars as $key => $value) {
        if(in_array( $key, $safeArray )) {
            if($start == $varsCount) {
                $end = '';
            }
         
            $update_string .= $key . '=' . '"' . $value . '"' . $end;
        }
            $start++;
    }

    if($start > 0) {
        $statement = "update users set " . $update_string . " where userId = '$user_id'";
        $query = $db->updateQuery( $statement );
        if($query) {
            $response  = array( "response" => 200 );
        } else {
            $response  = array( "response" => 500, "title" => "An unknown error occured, 
please try again");
        }

    }
wuzz
  • 364
  • 3
  • 10
  • Nope, it surely does not. – wuzz Nov 01 '20 at 19:12
  • 1
    Why not exactly? Prepared statements and parameters are the only way to be sure of preventing injection attacks. That's the standard approach and the same answer you'll get from everyone. So if you feel there's some extra dimension your situation then please make it clear. At the moment you are concatenating user input into the query and that's not safe. Restricting which columns can be updated is a validation measure but it does nothing to prevent injection attacks – ADyson Nov 01 '20 at 19:14
  • 3
    The only safety measure you have is verification for valid fields. There is no input sanitization whatsoever, making you subject to SQL injections. Consider using prepared statements instead of crafting queries based on user-provided data.. – Markus AO Nov 01 '20 at 19:14
  • @Markus AO Can you show me 1 example of how anything malicious could be done to that statement, so I can better understand. Because I am having a hard time grasping that obviously. Please and thank you – wuzz Nov 01 '20 at 19:21
  • : "It looks like you're writing your own ORM. Have you considered using one that's already written, tested, and widely supported like [RedBeanPHP](https://redbeanphp.com/), [Doctrine](http://www.doctrine-project.org/),  or [Eloquent](https://laravel.com/docs/master/eloquent)?" – tadman Nov 01 '20 at 20:26
  • @tadman I had some experience with Redbean and can say it's a bit out of date. the author is still struggling with namespaces and it makes it very hard to use RedBean in a modern environment. As a simpler alternative to Doctrine, I would rather suggest AtlasORM – Your Common Sense Nov 01 '20 at 20:51
  • @YourCommonSense I'll add that as a replacement. Thanks for the tip. – tadman Nov 01 '20 at 20:57
  • @tadman Speaking of that comment, the emoji does not render for me. It's a black box. Consider using a word instead of an emoji. – Dharman Nov 01 '20 at 22:23

2 Answers2

2

As the comments above suggest, it's worth using query parameters to protect yourself from SQL injection.

You asked for an example of how anything malicious could be done. In fact, it doesn't even need to be malicious. Any innocent string that legitimately contains an apostrophe could break your SQL query. Malicious SQL injection takes advantage of that weakness.

The weakness is fixed by keeping dynamic values separate from your SQL query until after the query is parsed. We use query parameter placeholders in the SQL string, then use prepare() to parse it, and after that combine the values when you execute() the prepared query. That way it remains safe.

Here's how I would write your function. I'm assuming using PDO which supports named query parameters. I recommend using PDO instead of Mysqli.

function updateProfile( $vars, $userId ) {
    $db = new Database();
    $safeArray = [
        "gradYear",
        "emailAddress",
        "token",
        "iosToken",
        "country",
        "birthYear",
        "userDescription",
    ];
    // Filter $vars to include only keys that exist in $safeArray.
    $data = array_intersect_keys($vars, array_flip($safeArray));

    // This might result in an empty array if none of the $vars keys were valid.
    if (count($data) == 0) {
        trigger_error("Error: no valid columns named in: ".print_r($vars, true));
        $response = ["response" => 400, "title" => "no valid fields found"];
        return $response;
    }
    
    // Build list of update assignments for SET clause using query parameters.
    // Remember to use back-ticks around column names, in case one conflicts with an SQL reserved keyword.
    $updateAssignments = array_map(function($column) { return "`$column` = :$column"; }, array_keys($data));
    $updateString = implode(",", $updateAssignments);

    // Add parameter for WHERE clause to $data. 
    // This must be added after $data is used to build the update assignments.
    $data["userIdWhere"] = $userId;
    
    $sqlStatement = "update users set $updateString where userId = :userIdWhere";

    $stmt = $db->prepare($sqlStatement);
    if ($stmt === false) {
        $err = $db->errorInfo();
        trigger_error("Error: {$err[2]} preparing SQL query: $sqlStatement");
        $response = ["response" => 500, "title" => "database error, please report it to the site administrator"];
        return $response;
    }
    
    $ok = $stmt->execute($data);
    if ($ok === false) {
        $err = $stmt->errorInfo();
        trigger_error("Error: {$err[2]} executing SQL query: $sqlStatement");
        $response = ["response" => 500, "title" => "database error, please report it to the site administrator"];
        return $response;
    }

    $response = ["response" => 200, "title" => "update successful"];
    return $response;
}
Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
  • Note I wrote this code but I didn't test it. If there are any small mistakes like missing semicolons, I'll leave that to you to fix. – Bill Karwin Nov 01 '20 at 20:03
2

In addition to the excellent Bill's answer, one little suggestion: always make your methods to do one thing at a time. If a method's job is to update a database, then it should only update a database and nothing else, the HTTP interaction included. Imagine this method could be used in non-AJAX context or without a web-server at all but from a command line utility. Those HTTP codes and JSON responses would look completely off the track. So have two classes: one to update the database and one to interact with the client. It will make your code much cleaner and reusable.

Also, never create a new connection to the database for the every query. Instead, have a ready made connection and use it for all database interactions.

function updateProfile($db, $vars, $userId )
{
    $safeArray = array( "gradYear", "emailAddress", "token", "iosToken", "country", 
"birthYear", "userDescription" );

    // let's check if all columns are safe
    if (array_diff(array_keys($vars), $safeArray)) {
        throw new InvalidArgumentException("Unknown columns provided");
    }
        
    $updateAssignments = array_map(function($column) { 
        return "`$column` = :$column"; }, array_keys($vars)
    );
    $updateString = implode(",", $updateAssignments);

    $vars["userIdWhere"] = $userId;
    
    $sqlStatement = "update users set $updateString where userId = :userIdWhere";    
    $db->prepare($sqlStatement)->execute($vars);
}

See, it makes your code slim and readable. And, above all - reusable. You don't have to make your methods bloated. PHP is a very concise language, if used properly

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
  • 1
    By the way, the code is using vanilla PDO and your $db is something else. Consider rewriting updateQuery() method the way it would accept the query and an array with parameters and execute it using prepared statement – Your Common Sense Nov 01 '20 at 21:22
  • Upvoted, I think it's good advice to make the function do one thing as you said. However, I think you should not use `array_values()` in the way you show, if your query parameters are the named parameter style. – Bill Karwin Nov 01 '20 at 21:52
  • 1
    @BillKarwin thanks, fixed I wanted to make it positional, to avoid possible collisions with characters disallowed in the placeholder names but changed my mind midway – Your Common Sense Nov 02 '20 at 05:10