you are viewing a single comment's thread.

view the rest of the comments →

[–]L_X_A 5 insightful - 3 fun5 insightful - 2 fun6 insightful - 3 fun -  (4 children)

The problem with perennial cookies are multiple. From the top of my head, the main ones relate to either someone stealing the cookie on the client (e.g. through javascript if you don't set the HTTPOnly flag), or the user logging in from a machine someone else has access to (e.g. library), or the user trying to connect to your site from a public network (hotel, Starbucks, etc.) and falling victim to an MiTM attack (no, HTTPS would not help you in this scenario. Unless your are using mTLS you wouldn't know whether you are talking to the MiTM or the legitimate user).

Also, depending on how you configured SSL, there are ways to downgrade it client-side so the attacker could sniff the communication even if they're not directly relaying information to and from the legitimate user.
Examples: https://access.redhat.com/articles/1232123 and https://freakattack.com/

But I think the perennial cookies are the least of our problems here.

I'm assuming that:

a) site_password is a static secret in your server which is used of all users. That is, user_1 gets MD5(concat(email_1, site_password)); user_2 gets MD5(concat(email_2, site_password)) and so on.

b) The cookies themselves are not encrypted.

If that's the case, with this simple attack I could impersonate all of your users:

  1. Register for your site with my email 1337.h4x0r@veryedgy.com
  2. receive MD5(concat(1337.h4x0r@veryedgy.com, site_password)) = myEmailDigest
  3. Run a parallelized MD5 crack, varying variable_salt on concat(1337.h4x0r@veryedgy.com, variable_salt)until I get myEmailDigest (with MD5 there are optimized ways of doing this).
  4. Now I know which variable_salt value will produce an MD5 hash which will be interpreted as legitimate by your server. Even worse, I potentially stumbled upon site_password itself. All I need now is a few more fake accounts and MD5(concat(fakeEmail, site_password)) results to be 99% sure.

From this point on, I have deduced site_password and thus have access to all of your users' accounts because all of them are authenticated through site_password.

I mean, it's good enough for a school/uni project. Or a site where it doesn't matter if one user can impersonate another (e.g. those "scrum poker" sites that don't require an account). But I wouldn't put it on anything which has persistent user data. Especially if it is used by people living in a country where GDPR applies.

[–]fschmidt 5 insightful - 3 fun5 insightful - 2 fun6 insightful - 3 fun -  (3 children)

These are valid issues. The javascript and MiTM attack issues are the least serious since they only risk exposing individual users. I don't see any solution for this with any system. A nonce just limits the time of exposure, nothing more. My sysadmin configured SSL and I assume he did it right. I have thought about the last issue you mentioned. I don't care for now while the sites are small and no one will bother. Later I would generate user passwords and store them in the user record and use that instead of a global salt, which solves this issue. If these things are addressed, I can still use persistent cookies. But I will worry about these issues in proportion to the size of the sites (number of users). In general, I don't have any site where security is really critical like a banking site would be. I don't keep credit card numbers or anything like that.

[–]JasonCarswell 1 insightful - 2 fun1 insightful - 1 fun2 insightful - 2 fun -  (2 children)

Can you add a little extra salt to make it seem a little more randomized? ie. Multiply the password by the/a time/date stamp or some other changing variable (title of current top thread title of FreedIt)?

cc /u/L_X_A

[–]fschmidt 2 insightful - 2 fun2 insightful - 1 fun3 insightful - 2 fun -  (1 child)

I thought more about this. What I will do is to generate a new password every time a user logs in. I will store that password in the database and include that password in the emailed URL. So no crypto or hash needed. And this completely eliminates the last problem that /u/L_X_A mentioned.

[–]L_X_A 2 insightful - 2 fun2 insightful - 1 fun3 insightful - 2 fun -  (0 children)

If you want to prevent the attack I described and still not have to store the passwords/salt-values in your server, you could go the authentication through encryption route.

Namely, you'd ditch the email and hash cookies for a single cookie containing the user's encrypted email address.

  • When the user registers with user_email, you send them an URL that will result in them receiving the cookie user-cookie = Encrypt(user_email, server_secret).
    I'd recommend a symmetric, strong (enough) cipher such as AES-256-GCM or ChaCha20Poly1305. You did choose MD5 as hashing algo earlier, and I'm assuming you did so for performance reasons. So it's up to you to judge which cipher would still accommodate your performance needs.

  • Don't forget to set the HttpOnly, Secure and SameSite flags.

  • On every request, the server would decrypt the ciphertext in the user-cookie's value using Decrypt(ciphertext, server_secret). If it matches the email of a user account, the authentication succeeded. Here's where you should watch out for your performance needs. This needs to be done on every request.

This solves the following problems:

  • You are no longer storing (plaintext) user information in the cookie, thus compliant with GDPR (see: https://www.gdpreu.org/the-regulation/key-concepts/personal-data/)

  • If someone steals the cookie, they won't be able to know what's in there.

  • If you chose a decent cypher, a plaintext collision attack as I described earlier becomes unfeasible.

This method still has the problem that every user's email is encrypted with the same key, though. So should someone be able to crack server_secret (very difficult depending on the cipher you choose, but still), they would be able to access every account they know the email of.

To circumvent this, you could extend this pattern with a Diffie-Hellman-based KDF functionality:

  • On your server, instead of the symmetric key as stated above, you generate and store a secret prime number which will be used in a "deferred" Diffie-Hellman key agreement. That is: server_secretPrime

  • When the user registers, you generate an ephemeral secret prime for the user, and calculate the user's public prime: user_publicPrime.
    You then store the following cookies:
    ** The encrypted email address: user-access = Encrypt(user_email, DH_KDF(user_publicPrime, server_secretPrime))
    ** The user-specific public prime used for the Diffie-Hellman key agreement: user-prime = user_publicPrime

  • When you receive a request from the user, you use the values stored on the cookie to authenticate them: Decrypt(user_email, DH_KDF(user_publicPrime, server_secretPrime))

This will ensure that a new key is used for every user, and it will not require you to store user-specific passwords in your DB.

It still leaves singular users vulnerable to someone stealing their cookie and getting access to their account in perpetuity (user-access + user-prime will always produce a valid ciphertext).

If you want to prevent this from happening, I see no other alternative than storing user_publicPrime in the DB and associating it with the user_email. Whereby you'd invalidate the cookie by generating a new value of user_publicPrime and storing it in the DB.

If you do this, you could of course forgo the DH_KDF pattern altogether by simply saving user-specific server_secrets. Then again, you'd be storing cryptographic material in a DB. Not exactly something you want to do. With the DH_KDF pattern, you can keep 1 server_secretPrime stored somewhere secure, while user_publicPrime can be stored in the DB without concern.