4
\$\begingroup\$

I read a lot about security mechanisms used in different APIs. But I'm still wondering if my own implementation is secure or not since it seems that there will ever be a risk of something you forgot ...

The scenario:

I have 2 PHP-running systems: Client and Server. There are multiple clients and one server but the communication is always between one client and the server. Both have their own user database (userID x may represent a user in both systems). The API is hosted on the server side so communication is always directed from client to server. Each client has a unique and numeric auto increment ID and a unique and secure key (UUID) which is only known by the client itself and the server.

When a logged in user on the client side uses the API the first time, he is asked to send his Name, Email and auto increment ID to the server. The Server stores the Data and generates a UUID for the user which is returned and passed by the user in further requests instead of its numerical auto increment ID.

The client calls the API script via https://www.server.de/api/api.php.

$host = 'https://www.server.de/api/api.php';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $host);
curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13');     
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_POST, 1);

In the API request, the clientID, the user UUID (when available, some requests won't need user authentication), the $request array as JSON string used in the API script, a generated nonce and a timestamp are send via post. The whole $post array is hashed with the private $key. The resulting hash is then added to the $post array.

// example request array
$request = array('action' => 'getList', 'listID' => 3);

$nonce = bin2hex(openssl_random_pseudo_bytes(10));

$param = array(
    'clientID' => $client_ID,
    'userID' => $user_UUID ?: 'noauth',
    'request' => json_encode($request),
    'timestamp' => time(),
    'nonce' => $nonce
);

$file = false;
$filepath = false;
if($request['file']){
    $filepath = $request['file'];
    $file = curl_file_create($filepath, 'image/jpg');
    unset($request['file']);
    $param['request'] = json_encode($request);
}

$param['hash'] = hash_hmac('sha256', json_encode($param), $key);

// NOTE: file and filehash are NOT in signature of the $_POST request 
// filehash is the signature for the content of the file
if($file){  
    $param['file'] = $file;
    $filecontent = file_get_contents($filepath);
    $filehash = hash_hmac('sha256', $filecontent, $key);    
    $param['filehash'] = $filehash;
}

curl_setopt($ch, CURLOPT_POSTFIELDS, $param );
$jsonstr = curl_exec($ch);
curl_close($ch);        

$returndata = json_decode($jsonstr, true);

When receiving an API request, the server reads the ClientID. If a client is found for that ID, the $post array is set up and hashed with the private key using hash_hmac and the SHA-256 algorithm and compared with hash_equals. If the hashes are equal, I assume that the API request is a valid request from a valid client and has not changed on its way (so data integrity is guaranteed). In addition I also look at the timestamp and the nonce. If the timestamp is older than 5 minutes or if the nonce was used before the server will reject the request.

In case of user authorization required, the server will also look at the user UUID and end with an error if no user was found or if the user has no rights to perform the request.

For files I get and hash the files content and compare it like I did with the $post array. If hashes are equal I check if the file extension $ext = pathinfo($_FILES["file"]["name"])['extension']; is allowed and store the file move_uploaded_file($_FILES["file"]["tmp_name"], 'dir/uploads/'.$_FILES["file"]["name"]);

Returning data from the API is printed as JSON string. echo json_encode($data);


What do you think? If anybody knows a better practice or sees any kind of security lack - I'm looking forward to learn from your answer.

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review, your post looks good, hope you get some good answers! \$\endgroup\$ – ferada Oct 30 '16 at 13:26
  • \$\begingroup\$ dont roll your own ! is there a reason you dont just use https ? optionally with client certificates? \$\endgroup\$ – Pinoniq Nov 2 '16 at 7:28
  • \$\begingroup\$ @Pinoniq The Server uses https. But I cannot guarantee that all of the clients do. Thats why the API requests are only directed from Client to Server: The client calls the API script via https://www.server.de/api/api.php. \$\endgroup\$ – lenny Nov 2 '16 at 7:32
  • \$\begingroup\$ you can guarantee it at the server only accepting https. \$\endgroup\$ – Pinoniq Nov 2 '16 at 8:03
  • \$\begingroup\$ @Pinoniq Yes but this is not an option. I have clients and they can run on http and https. I cannot and I dont want to force the use of https on client side. There is no need anyways since the requests goes from client to server always call the api.php on the server via https. \$\endgroup\$ – lenny Nov 2 '16 at 8:17
1
\$\begingroup\$

Couple things to look at here. First and foremost is ssl. If your server is hosted on https, then you can be assured that all communication with that server is already encrypted. It doesn't matter what the client site is hosted with as when it initiates communication with your server, your server dictates the terms. SSL support is widespread so you can be 99.9% assured that your clients will support it.

Further to the SSL point is that major players like Google are pushing for an https only web. With free SSL certificates available from Let's Encrypt, there's pretty much no reason not to be on SSL. You're looking out for everyone's security by requiring SSL, and it's best to be at the forefront of this.

Ok, the rest of this is assuming an unencrypted http connection.

You don't provide many specifics on the initial authentication where the server provides the UUID to the client. If you aren't hashing this initial authentication, then the credentials are exposed and no further security will matter.

I recommend you drop the client id from the post data. Since you have the UUID as a token, the client id becomes redundant. It's a best practice not to transmit any part of any user credentials after authentication because tokens can easily be changed, whereas credentials cannot. Credentials require a person to change them, whereas a token can be obtained automatically. Your description mentions that you're hashing the post data with a private key, so this isn't critical but is still a best practice.

With regards to your 'private' key, do you perhaps mean a 'public' key? Typically a public key is distributed for encryption. This allows for anyone to encrypt their data in such a way that it can only be decrypted by a private key. This private key is locked away on your server where it as safe as possible. This is how all SSL communications work and why it is so secure. If you're not going to https, best to mimic what is already best practice.

For your code, I have a bit of a concern with your hashing. The description states that you're hashing the entire post array with the private key. While it indeed look like you're hashing everything, you're only setting the result to another key in your array. That means that all the content in your array is not encrypted and can be accessed. That's great that you can verify whether it's been tampered with or not, but it won't stop someone from stealing the data; in fact it will have the terrible result of helping someone to break your encryption. You have handed them the code and the result; a Rosetta Stone for your private/public keys.

To fix your code, hash the $param array just before transmission with a public key and set it to $param. Then send the hashed $param to the server where it can be decrypted with the private key.

To wrap things up, you have some good parts here. You've identified that you need a unique UID for each authenticated user, and you've got some additional timestamp and one-time use checking. I'm not going to touch on those but rather leave you with a basic checklist of things to consider. You can then run through your own process to see if you've missed anything.

  • Does the client ever transmit unencrypted credentials?
  • Once encrypted, does the client switch over to a server provided authentication key which can more easily be deactivated or replaced if needed?
  • Is my key used for decryption ever in existence somewhere other than on my server?
  • Is there any way for someone to gain access to my private key other than myself and my application?
  • Can I uniquely identify each user consistently so that I can if needed reject unauthorized access and/or abusers?

You can debate and consider security until you're dead and cold but this should cover your basics. Note that if you require all access to be over SSL, you cover most of these points instantly. Good luck.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your detailed answer! I think there are some missunderstandings: first of all I do not encrypt and decrypt any of the transmitted data! The hash is only for the integrity. I rely on https on server side. Why would it be an improvement if the client uses https too? Because the client calls the api on the server (https) and the server never calls the client. The key I use for hashing is only known by the server and the client and never transmitted. I cannot drop the client ID since some requests don't have the user's UUID (eg. batchjobs) but the server has to know the client. \$\endgroup\$ – lenny Nov 4 '16 at 8:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.