How to protect sensitive data in URL's

From OWASP
Jump to: navigation, search

This is a control. To view all control, please see the Control Category page.

Often, we need to pass information from one page to another. The data can be passed with POSTs or GETs from a <Form>, or as key/value pairs in a URL that the user clicks on.

This section talks about how to protect the data that we are transferring from tampering. A few methods can be implemented.

The most straight forward method is to check the input for validity. If we expect the input data to contain only numbers, then we can check the input to verify that it contains only numeric data. While these validity checks are good for preventing unexpected program behavior, (i.e. a database query fails because it was expecting the id variable to be an integer) it does not protect against tampering.

Don't store it directly

If you want to reference a row in a database without leaking information (e.g. how many rows are in the database), you can simply store a randomly generated unique identifier in an extra column and reference that instead.

Hashing sensitive data

Hashing algorithims provide a simple way to detect tampering. For instance, when passing an id variable from page to page as a user is browsing, the program may expect that this id stays constant. By computing and sending a hash of the data, each successive page can verify, with a high certainty, that the value of the id variable has not been altered:

/**
 * Set this to something random, not a human readable password.
 * 
 * For example:
 * 
 * $hex = bin2hex(random_bytes(32));
 * $split = str_split($hex, 2);
 * array_unshift($split, '');
 * echo implode('\\x', $split), "\n";
 * 
 * The above will give you a value like "\xd8\x75\x26\xd5\x59\x45\x47\x1b\x02\x13\x13\xa5\xa8\x4d\x61\xd8\x94\xb0\x87\x60\x40\x2f\x29\x63\x2f\x13\x9c\xc3\x42\x88\xf1\xe5".
 * Use that for $secret instead of a human-readable password.
 * 
 * If you're running PHP 5 and don't have random_bytes(), use https://github.com/paragonie/random_compat
 */
$secret = 'MySecretWords';
$id = 12345;
$hash = hash_hmac('sha256', $id, $secret);

After hashing the id value with the secret, we get a hash value. This will be passed, along with the id value, to the next page for processing:

http://www.example.com/view_profile?id=12345&hash=d6b0ab7f1c8ab8f514db9a6d85de160a

In view_profile.php, we can detect tampering with the id value by re-hashing and comparing to the hash value from the previous page:

// See warning above
$secret = 'MySecretWords';
$id = $_REQUEST["id"]; //in this case the value is 12345
if (hash_equals(hash_hmac('sha256', $id, $secret), $_REQUEST["hash"])) {
  //no tampering detected, proceed with other processing
} else {
  //tampering of data detected
}

There is a disadvantage to using the hashing method discussed above; the value of id is visible to potentially malicious users. However, as long as the secret and the process for generating the hash (in this case, md5 is the hash algorithm, and the value hashed is the concatenation of $secret and $id) are unknown, malicious users will not be able to tamper with the id variable passed to the page.

Encrypting sensitive data

Next, we will discuss how we can use symmetric keys to protect sensitive data and at the same time do not reveal the actual data value.

The concept is very similar to hashing the value, but now instead we will use a symmetric key to encrypt and decrypt the data. Unless you know what the phrase "lattice attack" means off the top of your head, you probably shouldn't try to invent this on your own. No reason not to use a secure PHP encryption library.

/**
 * See above warning and advice about encryption keys. Don't just copy/paste ours.
 */
$key = "\xd8\x75\x26\xd5\x59\x45\x47\x1b\x02\x13\x13\xa5\xa8\x4d\x61\xd8\x94\xb0\x87\x60\x40\x2f\x29\x63\x2f\x13\x9c\xc3\x42\x88\xf1\xe5";
$message = "12345"; 

/**
 * This library is unsafe because it does not MAC after encrypting
 */
class OpensslAES
{
    const METHOD = 'aes-256-cbc';
    
    public static function encrypt($message, $key)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        
        $ivsize = openssl_cipher_iv_length(self::METHOD);
        $iv = openssl_random_pseudo_bytes($ivsize);
        
        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $encKey,
            OPENSSL_RAW_DATA,
            $iv
        );
        $mac = hash_hmac('sha256', $iv.$ciphertext, $authkey, true);
        
        return $mac.$iv.$ciphertext;
    }

    public static function decrypt($message, $key)
    {
        list($encKey, $authKey) = self::splitKeys($key);
        
        $ivsize = openssl_cipher_iv_length(self::METHOD);
        $mac = mb_substr($message, 0, 32, '8bit'); 
        $iv = mb_substr($message, 32, $ivsize, '8bit');
        $ciphertext = mb_substr($message, 32 + $ivsize, null, '8bit');
        
        // Very important: Verify MAC before decrypting
        $calc = hash_hmac('sha256', $iv.$ciphertext, $authkey, true);
        if (!hash_equals($mac, $calc)) {
            throw new Exception('MAC Validation failed');
        }
        
        return openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );
    }
    
    public function splitKeys($masterKey)
    {
        // You probably want RFC 5869 HKDF here instead
        return [
            hash_hmac('sha256', 'encryption', $masterKey, true),
            hash_hmac('sha256', 'authentication', $masterKey, true)
        ];
    }
}

$safe_data = OpensslAES::encrypt($message, $key);
$id = urlencode(base64_encode($safe_data));

To decrypt the information we received we will do the following:

$url_id = base64_decode(urldecode($_REQUEST["id"]));

$decrypted_data = OpensslAES::decrypt($url_id, $key);

The idea here is to url decode the input id value and follow by base64_decode it and then use OpensslAES::decrypt to get the actual data, which is 12345 in this case.

Further Reading on URL Encryption