API Security Check

Ask about a PHP problem here.
Post Reply
ScTech
Posts: 92
Joined: Sat Aug 24, 2013 8:40 pm

API Security Check

Post by ScTech »

Hello. Just finished my API and I would like a security check if possible. It's not completely done as of the distributing of the API key, and downloading of the data, but what I would really like feedback on is the handling of the data. I haven't installed SSL yet so there's no forcing it yet. It's actually in OOP but I remade it so mostly everyone should understand it. Edit: I just realized the lack of increment_api_request() calls. I'll be adding them later on.

api/index.php
<?php
// Ignore these.  Just for testing
error_reporting(E_ALL);
ini_set('display_errors', 1);

if($_SERVER['HTTP_USER_AGENT'] != "site Auto Updater")
        die(json_encode(array("status" => "Error", "message" => "Unauthorized API request.")));

if(!isset($_GET['action']))
        die(json_encode(array("status" => "Error", "message" => "Unauthorized API request.")));

// User is installing a script
if($_GET['action'] == "Create") {

        // Not done yet

// User is updating a script
} else if($_GET['action'] == "Update") {

        if(!isset($_GET['api_key']))
                die(json_encode(array("status" => "Error", "message" => "Invalid API key specified.")));

        if(!isset($_GET['script']))
                die(json_encode(array("status" => "Error", "message" => "Invalid script specified.")));

        require("functions.php");

        // Check validity of the API key.
        if(valid_api_key($_GET['api_key']) === false)
                die(json_encode(array("status" => "Error", "message" => "Invalid API key specified.")));

        // Check if the user has requested too many times today
        if(max_requests($_GET['api_key']) === true)
                die(json_encode(array("status" => "Error", "message" => "You have reached the limit of API requests today.")));

        // Check if user has valid access to the script
        if(valid_usage($_GET['api_key'], $_GET['script']) === false)
                die(json_encode(array("status" => "Error", "message" => "You do not have permission to update this script.")));

        // Get the script data
        $parts = get_file($_GET['script']);

        // If the requested script does not exist
        if($parts === false) {
                increment_api_requests($_GET['api_key']);
                die(json_encode(array("status" => "Error", "message" => "The requested script is invalid.")));
        // If we're updating the script or something
        } else if($parts == "Unavailable") {
                increment_api_requests($_GET['api_key']);
                die(json_encode(array("status" => "Error", "message" => "The requested script is currently unavailable for update.  Please try again later.")));
        }

        // Zip location - 0, Version - 1
        $part = explode(",", $parts);

        $num = 1;
        // If a temporary directory exists, increment the directory number to prevent issues
        while(is_dir("tmp/tmp_dir-{$num}"))
                $num++;

        $zip = new ZipArchive;

        // Get the zipped script file
        $res = $zip->open("../scripts/downloads/{$part['0']}");

        if($res === true) {

                // Extract the script into a temporary directory
                @$zip->extractTo("tmp/tmp_dir-{$num}/"); // @ to suppress an error that occurs randomly.  Google has no clue how to fix
                $zip->close();

        } else
                die(json_encode(array("status" => "Error", "message" => "Sorry.  There was an error preparing your update on our end.  If this error persists, please contact us at support@site")));

        // Create the tracking file for the cron job to access
        $track = fopen("tmp/tmp_dir-{$num}/track_file.txt", "w");
        fwrite($track, base64_encode(time()));
        fclose($track);

        // The script directory name afte the zip has been extracted
        $absolute_script = str_replace(" ", "_", $_GET['script'])."_v{$part['1']}";

        // Get the files in the extracted directory
        $dir = @scandir("tmp/tmp_dir-{$num}/{$absolute_script}");
        if($dir === false)
                die(json_encode(array("status" => "Error", "message" => "There was an error producing your update.")));

        unset($dir['0'], $dir['1']);

        // Move the files out of the directory for convenience
        foreach($dir as $src) {
                rename("tmp/tmp_dir-{$num}/{$absolute_script}/{$src}", "tmp/tmp_dir-{$num}/{$src}");
        }

        // Remove the now empty directory
        rmdir("tmp/tmp_dir-{$num}/{$absolute_script}");

        // Create a new iterator
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator("tmp/tmp_dir-{$num}/"));

        // Go through each file and encrypt the contents
        foreach($iterator as $file) {

                $extension = end(explode(".", $file));
                $exceptions = array("gif", "png", "jpg", "jpeg");

                // If the current file does not have an extension listed in the exceptions
                if(!in_array($extension, $exceptions) && $file != "tmp/tmp_dir-{$num}/track_file.txt") {

                        $code = file_get_contents($file);

                        // Code - 0, Size - 1, Random Key - 2, Offset - 3
                        if(isset($random_key)) {

                                $returned_data = encrypt_data($code, $_GET['api_key'], $random_key);
                                $encoded = explode("&", $returned_data);

                        } else {

                                $returned_data = encrypt_data($code, $_GET['api_key']);
                                $encoded = explode("&", $returned_data);
                                $random_key = $encoded['2'];
                                $offset = $encoded['3'];

                        }
                                
                        file_put_contents($file, $encoded['0'], LOCK_EX);

                }

        }


        increment_api_requests($_GET['api_key']);
        // Var1 - Tmp Directory
        // Var2 - Size
        // Var3 - Key
        // Var4 - Offset
        // Var5 - Version
        die(json_encode(array("status" => "Success", "message" => "Successful API request!", "var1" => base64_encode($num), "var2" => base64_encode($encoded['1']), "var3" => base64_encode($random_key), "var4" => base64_encode($offset), "var5" => base64_encode($part['1']))));

} else
        die(json_encode(array("status" => "Error", "message" => "Unauthorized API request.")));

?>
api/functions.php
<?php
// Just for testing.  Normal will be 5 or 10 probably
define("MAX_REQUESTS", 500);

// Function to check validity of the API key
function valid_api_key($key) {

        // Check for 64-digit hash format
        if(strlen($key) != 64 || !preg_match("#^[0-9a-f]{64}$#i", $key))
                return false;

        $database = "---";
        require("connections/select_connect.php");

        $key = mysqli_real_escape_string($connection, $key);
        $sql = mysqli_query($connection, "SELECT `id`,`access` FROM `users` WHERE `api_key` = '{$key}'") or die(json_encode(array("status" => "Error", "message" => "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
        mysqli_close($connection);

        if(mysqli_num_rows($sql) == 1) {
                $row = mysqli_fetch_array($sql);
                if($row['access'] == 0)
                        return false;
                else
                        return true;
        } else
                return false;

}

// Function to check if users have requested too much
function max_requests($key) {

        $database = "---";
        require("connections/select_connect.php");

        $date = date("l, F j, Y");
        $key = mysqli_real_escape_string($connection, $key);
        $sql = mysqli_query($connection, "SELECT `requests` FROM `api_requests` WHERE `api_key` = '{$key}' AND `date` = '{$date}'") or die(json_encode(array("status" => "Error", "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
        mysqli_close($connection);

        if(mysqli_num_rows($sql) == 1) {

                $row = mysqli_fetch_array($sql);
                if($row['requests'] >= MAX_REQUESTS)
                        return true;
                else
                        return false;

        } else
                return false;

}

// Function to check if the user has valid permission to use the script
function valid_usage($key, $script) {

        $database = "---";
        require("connections/select_connect.php");

        $key = mysqli_real_escape_string($connection, $key);
        $sql = mysqli_query($connection, "SELECT `valid_for` FROM `users` WHERE `api_key` = '{$key}'") or die(json_encode(array("status" => "Error", "message" => "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
        mysqli_close($connection);

        if(mysqli_num_rows($sql) != 0) {

                $row = mysqli_fetch_array($sql);
                $valid_for = explode(",", $row['valid_for']);

                if(!in_array($script, $valid_for)) {
                        increment_api_requests($key);
                        return false;
                } else
                        return true;

        } else
                return false;

}

// Function to get the file name of the requested script
function get_file($script) {

        $database = "---";
        require("connections/main/select_connect.php");

        $name = mysqli_real_escape_string($connection, $script);
        $sql = mysqli_query($connection, "SELECT `version`, `status`, `zip_location` FROM `downloads` WHERE `name` = '{$name}'") or die(json_encode(array("status" => "Error", "message" => "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
        mysqli_close($connection);

        if(mysqli_num_rows($sql) != 1)
                return false;
        else {
                $row = mysqli_fetch_array($sql);
                if($row['status'] == 0)
                        return "Unavailable";
                return $row['zip_location'].",".$row['version'];
        }


}

// Function to add on to the user's daily API requests
function increment_api_requests($key) {

        $database = "---";
        require("connections/select_connect.php");

        $date = date("l, F j, Y");
        $key = mysqli_real_escape_string($connection, $key);
        $sql = mysqli_query($connection, "SELECT `id` FROM `api_requests` WHERE `api_key` = '{$key}' AND `date` = '{$date}'") or die(json_encode(array("status" => "Error", "message" => "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
        mysqli_close($connection);

        if(mysqli_num_rows($sql) == 1) {

                require("connections/select_update_connect.php");
                $sql = mysqli_query($connection, "UPDATE `api_requests` SET `requests` = `requests` + 1 WHERE `api_key` = '{$key}' AND `date` = '{$date}'") or die(json_encode(array("status" => "Error", "message" => "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
                mysqli_close($connection);

        } else {

                require("connections/insert_connect.php");
                $sql = mysqli_query($connection, "INSERT INTO `api_requests` (`api_key`,`date`,`requests`) VALUES ('{$key}', '{$date}', '1')") or die(json_encode(array("status" => "Error", "message" => "Sorry.  It seems there was a communication failure on our end.  Please try again later.  If the issue persists, please contact us at support@site")));
                mysqli_close($connection);

        }

}


// Function to encrypt sensitive data
function encrypt_data($data, $api_key, $encoding_key = 0, $offset = 0) {

        // Random key generated by a combination of time, api key, and IP
        if(!isset($encoding_key) || $encoding_key == 0) {
                $offset = rand(0,32);
                $encoding_key = substr(hash("sha256", base64_encode($api_key.uniqid().$_SERVER['REMOTE_ADDR'])), $offset, 32);
        }

        $iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC);
        $iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
        $encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $encoding_key, $data, MCRYPT_MODE_CBC, $iv);

        $encrypted = $iv . $encrypted;

        $secured_data = base64_encode($encrypted);

        return $secured_data."&".$iv_size."&".$encoding_key."&".$offset;

}

?>
test_api.php
<?php

$parameters = http_build_query(array(
        "action" => "Update",
        "api_key" => "92bd30a6e35cc7a0666bfb7e1b23474c6b82d63fc2842d4b90876dd9e7487a54",
        "script" => "JQuery Chat"
));

$curl = curl_init("http://site/api/?{$parameters}");
curl_setopt($curl, CURLOPT_USERAGENT, "site Auto Updater");
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);  // For maintenance
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($curl);
curl_close($curl);

echo $response;
// Just an example, it output the following during one request
// {"status":"Success","message":"Successful API request!","var1":"Mg==","var2":"MTY=","var3":"MGMxNjdlOGE5N2JlMjgzNmYwYjBhODE1MTBmZTQ4NjI=","var4":"Nw==","var5":"MS4yLjE="}

?>
Any tips would be greatly appreciated!
<?php while(!$succeed = try()); ?>
User avatar
Temor
Posts: 1186
Joined: Thu May 05, 2011 8:04 pm

Re: API Security Check

Post by Temor »

That's quite a bit of code. It'll take some time looking through it and learning your code before I can make any judgement.
I'll get back to you.
ScTech
Posts: 92
Joined: Sat Aug 24, 2013 8:40 pm

Re: API Security Check

Post by ScTech »

Yea sorry about that. I tried to reduce as much as possible into functions. All but the last fuction should be good to skip. Granted I could be returning arrays instead of exploding some places, and that strlen() shouldn't be there in valid_api_key(), but those are minor details that I will nullify after everything is done. Even if there is an SQL Injection vulnerability, there's nothing of importance except maybe website URLs in the users table. Everything for the API is in its separate database. All I'm really interested in making sure that it is secure would be under the if(valid_usage()) check.
<?php while(!$succeed = try()); ?>
Post Reply