Cookie Theft

Browser cookies are very visible and can easily stolen or manipulated.

Some web browsers show all cookie data by looking in the preferences area. Lately, it has become more commonplace for browsers to hide this information, but that does not mean that cookie storage is less visible to an attacker. Stored cookies can also be stolen using Cross-Site Scripting (XSS).

Cookie data is also visible while in transit. It is sent in plain text in the headers of every request to the webserver and can be seen by an attacker who can observer network traffic. This is especially easy to do on an open WiFi network such as those commonly found at coffee shops and other businesses.


Imagine a website which makes the terrible security choice to store the user's login state in a cookie as plain text.

<?php
  setcookie('user_id', 42);
  setcookie('logged_in', true);
?>

The response to the user will be in plain text.

HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: user_id=42
Set-Cookie: logged_in=true

After that every request back to the webserver will display those cookie values in plain text.

GET /any_page.php HTTP/1.1
Host: 55.66.77.88
Cookie: user_id=42; logged_in=true

If an attacker can see cookie data, then it is easy for them to "steal" it. They can forge a request and include the cookie data as if it were their own. An attacker could set their own cookies to those values or forge new requests which include "user_id=42; logged_in=true". Alternatively, an attacker could modify the cookie values. In this example, they might try "user_id=1; logged_in=true" to see if that granted access to a different account.


Cookie Theft and Manipulation Preventions

The best prevention advice is simply not to put anything of value in cookies where it could be stolen. Only store non-sensitive data in cookies. For example, it is acceptable to store a user's language preference or a user's most recent choice for sorting a table of data. These are not sensitive and an attacker would gain nothing by also setting their language preference to 'English'.

Instead it is a better practice to store sensitive information in a server-side session. A session is usually a file or database record which contains the user's data. Sensitive data never leaves the server, so it cannot be observed in transit or in storage in the user's browser cookies. Instead, only a reference identifier ("session ID") is sent to the user's browser as a cookie. As you might guess, this session ID needs to be a long and unique number to prevent random guessing. While the data is not observable in transit, it is important to note that the session ID is observable in transit, and additional precautions need to be taken.

A best practice for cookies (and session IDs stored in cookies) is to set cookie expiration dates. Do not let cookies linger because the longer they are valid the larger the time period they can be exploited. When a cookie is created with an expiration date, the web browser will automatically delete the cookie after date.

It is also a good practice to set cookie's with a domain and path specified. By default, a cookie is available through a domain. If a more restricted subdomain ("store.site.com", "upload.site.com", or "members.site.com") or file path is specified, then the cookie will only be used for those URLs. This is an application of the Principle of Least privilege. Only give cookies meaning in the areas where they are required.

Because cookie data (and session IDs) can be stolen using Cross-Site Scripting (XSS), it is important to set cookies as being HTTPOnly. This setting makes cookies unavailable to JavaScript and prevents their theft using XSS.

Cookies can also be configured as "secure cookies" or "https-only cookies". These cookies are useful when the connection between the browser and webserver is over a secure, encrypted TLS/SSL connection. Secure cookies can only be set over a secure connection, and their data will only be sent back to the webserver over a secure connection. This is useful if some pages on a website use SSL while others do not because it ensures the cookies are not accidentally exposed by visiting a non-SSL page. Because the connection is encrypted, the cookies cannot be observed while in transit.


In PHP it is possible to provide only the cookie name and value, but there are additional parameters which allow setting of the expiration time, path, domain, and whether the cookie should be secure and/or httponly (true/false).

<?php
  setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
?>

For sessions, these options can be set as global default values in the php.ini file or (in PHP 7) as options when the session is started. (See cookie and session options)


Encrypt cookie data

For sensitive cookies, it is also possible to encrypt the cookie data using a two-way encryption algorithm. Not all algorithms are suitable, it must be possible to both encrypt and decrypt the values. The advantage of encrypting cookies is that if other protections fail the cookie data is never in plain text, neither in transit nor in storage.

Learn how to encrypt data with AES in PHP.


Sign cookies

Cookies can be "signed" as a protection against modification. While this could be done with any cookie value, it makes the most sense with encrypted cookie values because the original value is obscured.

The concept of "signing" is to calculate a checksum for the cookie data. (Basically, to run the cookie's value through an algorithm which returns the same string every time.) Then the checksum can be appended to the cookie data. Often this is done by using a "--" or other characters to separate the value from the checksum. When the cookie data is received, the string will be split on the "--" and the first part (the value) will be run through the same algorithm to make sure that the result is the same. If they match, the value is the same. If they do not match, then the value has been modified.

An example of signing a string in PHP:

<?php
  function signing_checksum($string) {
    $salt = "qi02BcXzp639"; // makes process hard to guess
    return hash('sha1', $string . $salt);
  }

  function sign_string($string) {
    return $string . '--' . signing_checksum($string);
  }

  function signed_string_is_valid($signed_string) {
    $array = explode('--', $signed_string);
    // if not 2 parts it is malformed or not signed
    if(count($array) != 2) { return false; }

    $new_checksum = signing_checksum($array[0]);
    return ($new_checksum === $array[1]);
  }
?>
Fork me on GitHub