Why should you only use Prefix - Cookies?

1. The general problem - cookies are difficult

  • A user of your site uses an insecure network. Sample: A WLan created to hack people.

     

    • Free Airport WiFi
  • The hacker creates a Welcome page with a Zero-iFrame (height:0, width:0 or x:-1000px, y:-1000px).
  • The src-attribute of that invisible iFrame: The http version of your site.
  • If your site doesn't use HSTS (most sites don't) and isn't preloaded (check the complete number of preloaded domains, first line of #7. Comments - most sites are not preloaded), the browser of the victim loads your site via http.
  • The hacker can (because the connection doesn't use https) change the content your site sends back: The hacker adds a cookie your site uses.
  • If the browser of the victim doesn't have that cookie, it's created. If the browser has already such a cookie, it's overwritten, if the cookie name, the domain and the path attributes are the same.
  • If the cookie exists and if the cookie has the HttpOnly- and / or the Secure-Flag, the hacker can remove both flags. The hacker can't read the old values, but he can override these.
  • If it is a session cookie: Then the hacker knows that session cookie.
  • The victim chooses "login", now the hacker is really happy: Logged in too, the shared session is now a Login-Session. Or you - the site owner: Are you the victim? Want to check your blog, create a new entry? Using that "free Airport WiFi"?

2. The solution: Prefix Cookies

  • Option to stop that: Use Prefix cookies:

     

    • __Secure-your-current-cookie-name
    • __Host-your-current-cookie-name
  • Really simple: Add the prefix to your existing cookie name. Check the cookies of this subdomain: __Host-check-your-website.server-daten.de is the standard-session-cookie (sent with HttpOnly, Secure, SameSite=Lax)
  • Both versions require https to be created. So the cookie must have the Secure attribute, a site loaded via https is required.
  • __Host- includes __Secure-.
  • __Host- requires a missing domain property and a path=/ property.
  • So cookies with __Host- prefix are only sent back to exact that host, not to a parent domain.
  • And: Both types can't be created or overwritten via http connections. That's the most important property.
Conclusion: If you use your own cookie names, a hacker is able to overwrite that cookie via a hacker-initiated http connection. If you use Prefix cookie names, that's not possible.

 

3. Check it - try to create 8 cookies via https and http

Click the following button. That function is executed:
function __create_Cookies() {

	console.log('Start __create_Cookies');
	document.cookie = 'cookie-with-secure=yes; secure';
	document.cookie = 'cookie-without-secure=no';

	document.cookie = '__Host-cookie-with-secure=yes; secure';
	document.cookie = '__Host-cookie-without-secure=yes';

	document.cookie = '__Host-cookie-with-secure-and-blogpath=yes; secure; path=/blog';
	document.cookie = '__Host-cookie-with-secure-and-domain=yes; secure; domain=check-your-website.server-daten.de';

	document.cookie = '__Host-cookie-with-secure-path-no-domain=yes; secure; path=/';
	document.cookie = 'tricky-cookie=via-' + location.protocol.replace(':', '') + '-' + Date.now().toString() + '; Max-Age=86400; path=/' + ((location.protocol.indexOf('https:') > -1) ? '; Secure' : '');

	console.log('Result');
	console.log(document.cookie);
	var d = document.getElementById('showCookies');
	if ((d)) { d.innerHTML = 'Result:' + '<p />' + document.cookie.toString().replace(/;/g, '<br />') + '<p />'; }


}

 

Open the console (Strg+Shift+I or Ctrl+Shift+I), then check the console and Web storage / Cookies or Application / Storage / Cookies.

 

 


 

FireFox creates 5 cookies. The first two without prefix, the __Host-Cookie __Host-cookie-with-secure with a reduced definition and the __Host-Cookie __Host-cookie-with-secure-path-no-domain with Secure, no domain and the path. And the tricky-cookie.

 

__Host- without a Secure-Attribute is dropped, path=/blog isn't allowed, a domain name isn't allowed.

 

Chrome creates only 4 cookies: The first two, the __Host-Cookie __Host-cookie-with-secure-path-no-domain with the exact match. And the tricky-cookie. Chrome follows the specification exact, FireFox is a little bit lax (sets the missing path=/).

 

Now try the same via

 

The domain is only a test domain without HSTS / Preload, the server is the same, the script is the same (see the source code). /.well-known/acme-challenge isn't redirected to https, so it's possible to use that subdirectory to test via http and https.

 

Result, first http, then https, then again http:

 

  • FireFox / http creates two cookies, the "cookie-without-secure" and the "tricky cookie". "cookie-with-secure" is ignored because it's http, not https. Same with all __Host-cookies: No https, no __Host-Cookies created.
  • Switching to https FireFox changes the "tricky cookie" to "Secure". Switching to http it's possible to overwrite the "tricky cookie". Same with older IE11 and Edge (Microsoft-Version).
  • Chrome is better: http - both simple cookies are created. https changes the "tricky cookie". Switching to http now the update of the "tricky cookie" is blocked.
  • But: The handling of FireFox / IE is correct - and bad. The protocol has no difference between ports and schemes. Different paths -> different cookies with the same name. Different ports -> if the cookie name is the same, it's one cookie.

 

Conclusion: A hacker can modify your Cookies without Prefix via http. Looks like it is a little bit browser-specific. It's impossible to modify your Prefix-Cookies via http. Because they are predefined secure, so it's impossible to change these cookies via an insecure scheme like http.

4. Cookies sent to the server

The test page shows the cookies the browser has sent back to the server.

 

Now check it to understand the problem:
  1. Cleanup all your cookies of xik-events.de (Web-Storage or Application - Cookies)
  2. Load the http - version http://xik-events.de/.well-known/acme-challenge/cookie-check.html
  3. Create the cookies. You should see some lines:

     

    • Your browser sends the following Cookies to the server:
    • No cookies sent back.
    • ...
    • Result:
    • cookie-without-secure=no
    • tricky-cookie=via-http-1595177366813
    You don't see prefix cookies. Because it's impossible to create prefix cookies via http.
  4. Open the https version - https://xik-events.de/.well-known/acme-challenge/cookie-check.html
  5. Now you see:
    • Your browser sends the following Cookies to the server:
    • 1: cookie-without-secure: no
    • 2: tricky-cookie: via-http-1595177366813
  6. So you use https - but the hacker knows your session cookie "tricky-cookie", created via http and sent back to https
Conclusion: If a hacker / WiFi-Owner is able to initiate a connection of your browser to a http page, the hacker can add cookies via http to the answer (may be a http status 301 - redirect to https). Then your browser uses https - and the hacker-created cookie is your new https session cookie.

 

That doesn't work with Prefix-Cookies. So use Prefix-Cookies everywhere. Especially Session-Cookies.

5. But what happens if clients don't support Prefix cookies? Use signed Cookies with a timestamp

The solution with Prefix-Cookies has one limit: The client browser must support it.

 

Additional solution: Use signed Cookies, include a timestamp, combined with prefixes.

 

What's that? Normally, a session cookie looks like
  • session-cookie-name=FF22EDCB-312A-4510-9644-1CCD7DB7863C
The string "FF22EDCB-312A-4510-9644-1CCD7DB7863C" is the search string to find a row in the session table. If no row exists, a new row with that identifier is created.

 

So it's easy to create a lot of "good-looking session cookie values". Every programming language has tools to do that. And it's not really relevant if you use one of these strings or three combined. An insecure WLan can add a cookie with such a value, the browser of the victim sends that cookie back. The server says: "Great, there is a valid session cookie, let's start a session" - the user and the hacker have the same session.

 

So the main problem: A client or hacker can create own session identifiers. These are always valid and they don't have an expiration. These are simple strings.

 

But: That doesn't work if the session-cookie-value is a combination of the search string and ... a signature + a timestamp.

 

  • Create such a random list of bytes, used as search string
  • May be add the host name (some additional bytes), the port, some of the standard header values of your clients: Accept-Encoding, Accept-Language, User-Agent. That fixes the cookie to the port (official, Cookies are port-independend). And a hacker must know and use the same combination of Accept-Encoding, Accept-Language and User-Agent. Be careful adding the "Accept" - header: Browser send different headers if it is a website, a CSS or JavaScript. Or exclude these files from the signature validation.
  • Add the number of Ticks (DateTime.Now().Ticks) or a Unix-Timestamp to that byte array. First, save that int64 / long into a local variable
  • Use a private / public key pair (EC 256) to sign these bytes
  • Create a new byte array - search bytes + signature bytes
  • Transform that to Base64. Add a dot and the number of Ticks or your Unix-Timestamp at the end. That's your cookie value
  • May look like

     

    __Host-check-your-website.server-daten.de=hedP6RE6s0mG6RpBAAtXfNrlHU6zye5Oj6oJufMFmyY3B5DmL2YwTaJ6vrrxOv5rPIWCZVUO6NYI8C2ewoG+pFZSrYa57setZqAAJ2br7zyVmW+6nbRk6hVZdKrS8m0WbqYpfrt0P4KQ7LEIA7Pekw==.637313057071275660

     

    That's the Base64 of two Byte-Arrays:

     

    hedP6RE6s0mG6RpBAAtXfNrlHU6zye5Oj6oJufMFmyY3B5DmL2YwTaJ6vrrxOv5r - the Session-Identifier

     

    PIWCZVUO6NYI8C2ewoG+pFZSrYa57setZqAAJ2br7zyVmW+6nbRk6hVZdKrS8m0WbqYpfrt0P4KQ7LEIA7Pekw== - the 64 Byte signature

     

    637313057071275660 = 2020-07-25 20:28:27 - so the timestamp is already invalid. If you use that complete value (FireFox Web storage, Chrome Application, there you can change an existing cookie value), you see: A new session is started.

     

    So using such a combination of Session Identifier, timestamp and signature of both stops the re-use of session identifiers.
If such a cookie with a signature is sent to the server:
  • Check, if the length is correct, split the timestamp.
    • Check, if the timestamp is too old (> one hour) -> throw the cookie away, start a new session
    • Check, if the timestamp is in the future. Then it's not a server generated cookie. Throw it away, start a new session.
    • Check, if the timestamp is older then 15 minutes. If yes, extract the session identifier, create a current timestamp, create a new signature, send the result back to the user
  • Transform the Base64 to a byte array
  • Split the byte array in two parts - search bytes and signature bytes
  • Verify that the signature is valid. The public key, the data (your search bytes, the timestamp, may be the additional strings) and the signature must match.
  • If yes, that list of search bytes, transformed to a Base64-String, is a valid, server created Session identifier
  • If no, throw that away and start a new session with a new server-created Session identifier and a valid signature. Send the new session cookie back to the client. Via https, with the Secure-Attribute.
Result: A hacker can't create valid session variables because he can't sign the data. Valid session cookies are only server-created and only sent via https.

 

A hacker can load the site to get a new, valid cookie. But if he don't use the same headers (Accept-Encoding...) like the victim, he can't use that cookie.

 

If you use such a timestamp (included in the bytes to sign), the hacker can't change that timestamp.

 

Try it

  • Check the cookies of this page. Select the __Host-check-your-website.server-daten.de cookie. With FireFox / Chrome it's possible to change the cookie value.
  • Remove the last number, reload the page: The session is too old -> you will see a new session value.
  • Change the last number, reload: The signature is invalid. There are two options: Same session with a new timestamp and a new signature or a new session.
  • Remove one character from the base64-Part: The base64 is invalid, a new session is created.
  • Change one character from the base64-signature-Part: The signature is invalid -> two different options.

6. The problem of Session fixation - create a new session after a login or add a second cookie

The general problem is called Session Fixation. A hacker defines a cookie he knows. The web application allows it that the hacker can send that cookie to a user (that's the main problem you have to fix). The user goes to the web application and uses that cookie.

 

So it's a general problem, not only of web applications with a login.

 

If your web application has a login: Start a new session after the database has confirmed the combination of username and password. Then the old session identifier is invalid.

 

That may be tricky if an environment (like .NET) doesn't support that directly. But with .NET, it's possible.
  1. After a login, use SessionStateUtility.RemoveHttpSessionStateFromContext to remove the Session State from the current context
  2. You need your own ISessionIDManager implementation. With that, use ISessionIDManager.InitializeRequest, ISessionIDManager.CreateSessionID and ISessionIDManager.SaveSessionId to create a new Session-Identifier and a cookie, that's sent to the client
  3. Then you can create a new HttpSessionStateContainer and add it with SessionStateUtility.AddHttpSessionStateToContext to your current context.
But that solution has one problem. That "new session" doesn't work with existing solutions, it doesn't work with the out-of-process SessionStateMode.StateServer. The new session is isolated, you can use the Me.Context.Session-Object, but the next call doesn't see these values (user is logged in). Solution: You need an "one-step own session handling".
  1. Create a NameValue-Collection with the values you want to store in the session: User-Id, Token to identify the user against the database
  2. Add that collection to the cache - with the new Session Identifier as Identifier.
  3. Redirect to the current page, so the new Session is used
  4. Every call must check, if there is such a cache object with the own Session Identifier.
  5. If yes, import these values into the current session.
  6. If a user does a login, the application is restartet and the next call hits the new process, that doesn't work. The restart kills all cache values.

     

    To work:
    1. Create a file with the Session Identifier as file name, save the name value collection to disk.
    2. Start processing a page: If there is no cache object, the restart of the application may be the reason. So if the application is "new enough", check the disk if there is such a file.
    3. To block too much disk checks: Add an App-Start key to your Application state with the Ticks the application has started.
    4. Check the disk only if the Application is "young enough".
    5. The cache entry of the name value collection should have a SlidingExpiration of one minute and a CacheItemRemovedCallback that checks, if such a file exists. If yes, that file is deleted. But that works without blocking the user interaction
Another solution: After a login, create a second cookie with a new random value. Send that cookie to the client and store the value in the session. Every call later the user sends back that cookie. Compare it with the value stored in the session. If there is a mismatch: Abort the session.

 

Both solutions: After a login a new secret is created and shared between the server and the client. If https is used (that's required), then the hacker doesn't know that new secret.