Demystifying Cookies and Tokens Security

Hello everyone, this is my first article for SecurityFlow. I’ve decided to run this blog to write more often and share more about my AppSec related researches, and other security related articles.

TL;DR

When I started learning about different web authentication and authorization methods, I got quite confused as there are lots of different implementations and the problem is that there are not many interactive labs and materials to understand their security perspective.

After long research and solving my misconceptions, I decided to write this blog to better explain this topic to everyone, so that they can learn more about the implementation, features, and security risks of different authentication models.

Table Of Contents

  • Authentication & Authorization
  • Sessions & Cookies
    • Concepts
    • SameSite Cookies
  • Token Based Authentication
    • Concepts
  • Cross-domain attacks (CSRF, CORS, etc.)
  • Real world vulnerability explained
  • Conclusion

If you have time (or struggle to understand anything) I highly recommend checking out the slides and watching the video of my presentation. Also, you can access the labs in this repo.

Authentication & Authorization

Authentication is the process of verifying a user’s identity. the one that you get 401 Unauthorized error for.
Authorization is the process of verifying a user’s access control and permission. the one that you get 403 Forbidden error for.
These errors are generally generated by the webserver. in modern web applications, custom errors can be used.

Authentication Models

There are various authentication models, such as:

  • Cookie-based
  • Token-based
    • JSON Web Tokens
    • OAuth
  • Single Sign-On
  • SAML

Sessions

The user’s session is stored server-side (stateful). Sessions are mostly stored in:

  • Database e.g Postgres, MongoDB
  • Cache e.g Redis, Memached
  • File System

Users are identified by their session ID, if the proper value is provided they are granted to access their needed resources.
Sessions should be randomly generated and carry no sensitive data, otherwise, issues are coming up as they can be guessed, or if compromised sensitive data can be extracted from it.

Cookies

Cookies are used for session management, personalization, user tracking, etc.
They consist of names, values, and optional attributes. They are set with the Set-Cookie header by the server.
Here is an example workflow:

  • User requests to login to the application (redacted.com) using his credentials.
  • The application (redacted.com) validates the credentials and responds with sessions and cookies.
  • User can access his/her needed resource by sending the authentication cookie.
  • User can’t access another resource on a different subdomain (a.redacted.com) with the same authentication cookie.

Set-Cookie

Cookies have different attributes, from which we are going to explain a couple in great detail. Here is an example Set-Cookie header set by the application server.

Set-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2021 07:28:00 GMT; Secure; HttpOnly
Set-Cookie: id=a3fWa; Domain=redacted.com; Path=/; SameSite=Strict

The user will send back the received cookies with the Cookie header. There are several cookie attributes we need to discuss, as they are quite tricky.

Cookie Attributes

As mentioned before, Cookies consist of names, values, and optional attributes. Here is the list of optional attributes a cookie might have:

  • Secure
  • HttpOnly
  • Expires
  • Domain
  • Path
  • SameSite

Secure

A cookie is only sent to the server when a request is made with the https: scheme. Therefore MiTM attacks are prevented in this way.

HttpOnly

Forbids JavaScript from accessing the cookie, for example, through the document.cookie property. This mitigates attacks against cross-site scripting cookie stealing, as the attacker is not able to access the victim’s cookies to take over their account.

Expires

The maximum lifetime of the cookie as an HTTP-date timestamp.
If unspecified, the cookie becomes a session cookie. A session finishes when the client shuts down, and session cookies will be removed.

Path

A path that must exist in the requested URL, or the browser won’t send the Cookie header.
The forward-slash (/) character is interpreted as a directory separator, and subdirectories will be matched as well: for Path=/docs/docs, /docs/web, and /docs/web/http will all match.

Domain

Host to which the cookie will be sent.

  • If omitted, defaults to the host of the current document URL, not including subdomains.
  • Contrary to earlier specifications, leading dots in domain names (.example.com) are ignored.
  • Multiple host/domain values are not allowed, but if a domain is specified, then subdomains are always included.

SameSite

Controls whether a cookie is sent with cross-site requests, providing some protection against cross-site request forgery attacks.
SameSite cookies are very vast, I’m going to explain them in-depth, however, to better understand them, you need to understand some prerequisites.

First-Party vs. Third-Party Cookie

If you are in the a.com website, and you attempt to access a service from the same domain name a.com, cookies generated will be considered first-party cookies.

Whereas, if you visit a website a.com but that page includes content (image, iframe, etc.) from a different domain name b.com, cookies set by b.com will be considered third-party cookies because they come from a different name than in the URL bar: a.com.

Same-Origin vs. Same-Site

Origin

Origin is the combination of scheme, hostname, and ports.

https://www.example.com:443
  • https is the scheme.
  • www.example.com is the hostname.
  • 443 is the port.

Websites that combine the same scheme, hostname, and port are considered “Same-Origin”. Everything else is considered “cross-origin”.

Site

Site is the combination of (e)TLD and the domain part just before it.

https://www.example.com:443
  • .com is the (e)TLD.
  • example.com is the site.

eTLD

Let’s explain eTLD with a question from you, can you spot the Site in the following URLs?

https://www.example.co.uk:443
https://holybugx.github.io:443

From our previous understanding of the term “Site”, we know that it’s the combination of TLD and part before it.

However, for domains like .co.uk .github.io etc. just using the TLD of .uk or .io is not enough to find the site. and there is no algorithm to determine them.
That’s why a list of “effective TLDs”(eTLDs) was created. You can check the complete list here.

After reviewing the eTLD list we can understand the .co.uk and .github.io are eTLDs, therefore based on what we knew about sites, we can conclude that example.co.uk and holybugx.github.io are the sites.

If you are still confused about site and origin differences, I’d recommend you to check out this website to easily compare your examples.

SameSite Cookies

Now that we learned about First-Party & Third-Party cookies as well as Same-Origin & Same-Site we can further continue discussing SameSite cookies.

The SameSite cookie controls whether a cookie is sent with cross-site requests. The SameSite attribute can be set with the following values: Strict, Lax, None.

Strict: The cookie will not be sent along with requests initiated by third-party websites.

Lax: The cookie will be sent along with the GET request initiated by third-party websites.

None: Allows third-party cookies to track users. however, needs the Secure flag as well.

Lax Notes

  • If the SameSite cookie is not set, the default value will be set which is Lax. (Firefox uses None by default)
  • To send a cookie with a GET request, GET request being made must cause a top-level navigation.
  • Resources loaded with img, iframe, and script tags do not cause top-level navigation, thus cookies set to Lax won’t be sent with them.

Schemeful SameSite

Schemeful SameSite is where the Same-Site term relies on the HTTP scheme as well, but it’s only supported on Chrome 89+ at the time.

Labs

I have hosted several labs in this repository, you can install the labs locally and start playing with them. and if you have no idea what to do, I recommend you to watch the presentation video as I go through the labs.

Cookies Security Issues

  • Cross-Site Request Forgery – CSRF
  • Cross-Site Scripting – XSS
  • Cross-Origin Resource Sharing – CORS
  • Other rare attacks e.g Session Fixation, Cookie Tossing, etc.

The main issue with Cookie-based authentication is that the browser sends cookies by default, resulting in various cross-domain attacks such as CSRF and CORS. XSS is dangerous if HttpOnly attribute is not used, which leads to account takeover as the attacker is capable of stealing the victim’s cookies.

Token-Based Authentication

Token-based authentication is a protocol that allows users to verify their identity, and in return receive a unique access token. During the life of the token, users then access the website or app that the token has been issued for, rather than having to re-enter credentials each time they go back to the same webpage, app, or any resource protected with that same token.

  • Tokens are usually stateless (not stored server-side)
  • Tokens are signed with a secret (tamper proof)
  • Tokens are both opaque (doesn’t contain sensitive data) and self-contained
  • Tokens can be simply revoked
  • Tokens are commonly sent in the Authorization HTTP header
  • Tokens are used in SPAs, APIs, and various Web&Mobile Apps

Here is the example token-based application workflow:

  • User requests to login to the application using his credentials.
  • The server validates the entered credentials and creates a token.
  • User can access his/her needed resource by sending the issued token.
  • The server validates the token for each request.

Storages

JWTs (token) are stored in localStorage and sessionStorage. The only difference between them is that localStorage doesn’t have an expiration date, while sessionStorage gets cleared after closing the browser tab.

JSON Web Tokens

A JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret or a public/private key pair.

JWTs consists of three parts separated by dots (.) which are:

  • Header
  • Payload
  • Signature

Header

The header typically consists of two parts: the type of token, which is JWT, and the hashing algorithm that is used, such as HMAC SHA256 or RSA.
For example:

{
  "alg": "HS256",
  "typ": "JWT"
}

Then, this JSON is Base64Url encoded to form the first part of the JWT.

Payload

The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registeredpublic, and private claims.
An example payload could be:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Then, this JSON is Base64Url encoded to form the second part of the JWT.

Signature

To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.
For example, if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

Putting all together

The output is three Base64-URL strings separated by dots that can be easily passed in HTML and HTTP environments while being more compact when compared to XML-based standards such as SAML.

CSRF & CORS Failure

As mentioned, cookies are sent by default on all browsers that results in various cross-domain attacks such as CSRF, CORS, and, XSS. However, in token-based authentication, there is no authentication cookie.
This implementation results in a NO to cross-domain attacks such as CSRF and CORS.

CSRF

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. a successful CSRF attack can force the user to perform state-changing requests like transferring funds, changing their email address, and so forth.

CORS

Cross-Origin Resource Sharing (CORS) is an HTTP-header-based mechanism that allows a server to indicate any other origins (domain, scheme, or port) than its own from which a browser should permit the loading of resources. CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, to check that the server will permit the actual request.

CORS Preflights

Some requests are simple and some are called preflight. Let’s first discuss what are the simple requests?

  • If there is no custom HTTP header (anything besides Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width)
  • If HTTP verbs are GET, POST, and Head
  • If HTTP verb is POST and the content-type is text/plain, multipart/form-data, application/x-www-form-urlencoded

Anything other than the mentioned specification is called a preflight request.

Code Example

Simple Request

The following XHR code is considered a simple request because it obeys the rules we mentioned. this is a simple GET request with no additional HTTP headers.

const xhr = new XMLHttpRequest();
const url = 'https://domain.tld/api/getUserInfo';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();

Preflight Request

The following XHR code is considered a preflight request as the content type is different from the allowed ones, and there is an extra HTTP header being set called X-Custom.

const xhr = new XMLHttpRequest();
const url = 'https://domain.tld/api/editUserInfo';

xhr.open('POST', url);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.setRequestHeader('X-Custom', 'test');
xhr.onreadystatechange = handler;
xhr.send('{"fname":"John"}')

If the request is a preflight request, then before sending the request to the application server, there will be an initial OPTIONS request. Here is an example workflow on the XHR above:

As an attacker, you might think: “The authorization header is not sent by default, but can I force the browser to send it?

The main issue is that an attacker doesn’t know the victim’s token to put into his XHR code snippet. Therefore the header will be sent as null or with an invalid value.

const xhr = new XMLHttpRequest();
const url = 'https://domain.tld/api/editUserInfo';

xhr.open('POST', url);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.setRequestHeader('Authorization', '???');
xhr.onreadystatechange = handler;
xhr.send('{"password":"Hacked!"}')

As observed in the XHR code above, we can only provide the Authorization header, however, for the value, we have no idea what to put in, which stops all possible cross-domain attacks.

Real World Vulnerability

A while back, I found an interesting vulnerability related to this article, that I think is worths sharing. This is the story of “How I chained a misconfiguration and an XSS to achieve a working CSRF on all subdomains.”

The application was using CSRF tokens in the POST body for all state-changing requests. the parameter was named X-CSRF-Token. when it comes to CSRF there are several tests most hackers do, such as:

  • Removing the CSRF parameter
  • Removing the CSRF parameter value
  • Using CSRF tokens from other accounts
  • Token length tampering
  • Verb tampering

As expected none of those methods worked well for me. however, I tried another technique that I rarely see anyone does. I removed the X-CSRF-Token from the POST body and added it in the Cookie header as a parameter. Interestingly, It worked! although, it’s not a vulnerability on its own as we don’t access other user’s CSRF tokens.

I tried the mentioned CSRF bypasses while having the parameter in the Cookie header. Surprisingly, I realized that the X-CSRF-Token length was the only thing that matters.
This is an issue for sure, probably developers were thinking that “this is not an issue as an attacker can not set a cookie for other users.” but this is totally wrong.

Using Cross-Site Scripting we have access to the victim’s DOM and we can tamper with it. therefore we can easily set a document.cookie.

I tried to find an XSS on their main domain (as the CSRF misconfiguration was there) however, I wasn’t able to do so. then I realized I can do the second trick which is to sign my own cookies with the domain cookie attribute. this is huge as if I find an XSS on any out-of-scope subdomain which is not that hard either, I can sign a cookie for all victims and get a site-wide CSRF on their main domain (and possibly other subdomains).

I found an XSS in a couple of hours, and using the following payload I was able to achieve what I always wanted:

<script type="application/javascript">
var cookieName = 'X-CSRF-Token';
var cookieValue = 'abcdefghijklmnop';
document.cookie = cookieName + "=" + cookieValue + ";domain=.redacted.com; path=/";
</script>

Voila! A chain of multiple misconfigurations and an XSS on any out-of-scope subdomain results in a valid CSRF on all subdomains.

Conclusion

The implementation takes a huge role in how the application behaves. Cookies implementation doesn’t take a huge role in cross-domain attacks since they are sent by default on browsers. however, token-based authentication can be implemented in various methods, which makes a whole lot of difference.

Storing tokens in the Cookie header

There are several issues if the application stores the JWT tokens in the Cookie header. The most important issue is that CSRF and CORS issues are still a possibility as the cookies will be sent by default on all browsers. The other issue is that XSS can steal the cookies if not signed as HttpOnly. It’s good to note that even if the HttpOnly cookie attributes are in use, the attacker has access to the victim’s browser DOM, meaning that the attacker can send XHR requests on behalf of the user

Storing tokens in the Authorization header

This implementation method is way more secure than the other method, It makes use of an authorization header in the HTTP request, this header can be set as default (Authorization) or custom (X-Auth).
As there are no more authentication cookies so CSRF and CORS will not be an issue anymore. However, XSS usually leads to direct account takeover as the attacker is capable of accessing the tokens in localStorage.

CSRFCORSXSS
Cookie-Based Authentication❗️
Token-Based Authentication
  • This table is based on proper token-based authentication. Authorization: Bearer <token>
  • XSS in cookie-based applications doesn’t lead to direct account takeover if HttpOnly attribute is in use.
  • XSS in token-based applications usually leads to direct account takeover after stealing the token from localStorage.
  • CORS and CSRF is not possible in properly implemented token-based applications.

That’s all thanks for reading! I hope that you’ve learned something new and your misconceptions are gone now. If you had any further questions feel free to contact me @HolyBugx.

3 Replies to “Demystifying Cookies and Tokens Security”

  1. Hey Emad , I was confused while learning broken authentication topic of the OWASP top 10 , but by reading this article everything became clear to me . Thank you a lot .

Leave a Reply

Your email address will not be published. Required fields are marked *