0

Not fully sure if these types of questions are allowed here, but don't see anything against it in the help centre so I'm assuming it is. Please point me to a better place to ask if it's not!

I'm making an account system for my website and trying to do my best to protect accounts from several types of attacks but a bit worried, possibly paranoid, that I'm going to miss some. I've not completed the sign-in page yet, but this is the code that allows users to sign up https://hastebin.com/isuhiwavex.php. Currently, I'm trying to:

  • Protect it from bots, brute-forcing and other similar suspicious activity using ReCAPTCHA v3 (allowing scores higher than 0.5)
  • Requiring passwords be more than 8 characters and contain both upper and lower case characters
  • Encrypting passwords with Argon2id with paramaters that take ~0.5 seconds for my server to process (memory cost 20000, time_cost 17, threads 2)
  • Making sure emails are in a valid format for XSS and general validity
  • Formatting HTML special chars in the username, also for XSS
  • Making sure accounts can't be created with usernames/emails that already exist

When I make the login page, the only extra I plan on adding is that only 3 login attempts can be made per minute per IP. I realise an IP isn't the best way of identifying people, but wasn't sure what else I could get that could be stored in my database easily.

6
  • 1) consider wrapping your password_hash attempt in a try{} catch() to intercept hashing failure; 2) as far as I'm aware the threads option has no impact on the outcome; 3) since you're using Argon2id depending on your hardware the memory_cost seems low (20kb?) and time_cost seems high - on hardware that isn't too busy you'd want tens to hundreds of megabytes and then tune the ops_limit to reach the longest delay possible that is still acceptable to your user/s (min 4)
    – brynk
    Oct 18, 2021 at 3:39
  • The memory_cost is apparently in kikibytes so I believe it’s 20mb I’ve set, not kb. Curious what would cause a hashing failure?
    – hopperelec
    Oct 18, 2021 at 6:58
  • you're correct about it being kikis - it's hinted at in the comment above get_options() adjacent .. the current php codebase seems to throw an error if threads != 1
    – brynk
    Oct 18, 2021 at 7:20
  • I don't seem to get an error with two threads
    – hopperelec
    Oct 18, 2021 at 14:26
  • can you please run php -v in your server environment?
    – brynk
    Oct 18, 2021 at 20:51

1 Answer 1

0

I'll assume that you've asked for another set of eyes to pass over your logic and its code impl that you linked to in your question. (If you edit your question with more detail then I might update this answer.)

The use of ReCaptcha has the potential to leak PII depending on what's included in your signup form. TODO: link to terms. You might consider the use of hCaptcha or review which fields are exposed.

The requirement for upper- and lower-case characters is unnecessary if the user is providing a high-entropy password. In my opinion you'd get more mileage imposing a longer minimum length, however, this may frustrate some users further.

There are a couple of things worth noting in the implementation ...

  if (strlen($_POST["username"]) < 4 ||
      strlen($_POST["username"]) > 32 ||
      strlen($_POST["pass"]) < 8 ||
      preg_match("/^(?=.*?[a-z])(?=.*?[A-Z]).*$/",$_POST["pass"]) == 0 ||
      preg_match("/^.{1,128}@.{1,63}\..{2,24}$/",$_POST["email"]) == 0) {

and

  $_POST["pass"] = password_hash(
    $_POST["pass"]
    , PASSWORD_ARGON2ID
    , ['memory_cost' => 20000
        , 'time_cost' => 17
        , 'threads' => 2 ]
  );

Prob here is password length is unbounded, and will wind up at the maximum length that php.ini or other config will allow a POST to be (depending on what type of <form> you've presented your user/s). I propose limiting this maximum to some sensible upper limit (eg. 4096, ie, 4k) to reduce opportunity for resource depletion attacks. (This gets fed into the password hashing routine.)

Also that regex match on the password doesn't seem right to me, but I'm a bit rusty with PHP and regex. It seems that the password must have a lower-case followed by a upper-case char, or it's not valid. (Assuming (?= is a positive look ahead, but happy to be corrected.)

(caveat: I'm not certain exactly if PHP uses the Libsodium impl that I've linked to - I might follow this up at some point when i have more time...)

I checked the Libsodium function that (probably) underlies password_hash() and it does indeed limit Argon2id to a single lane and thread. I haven't been able to find much on this Libsodium design decision, however, on reflection there's a "risk management" argument for this: an errant process won't leave your system unresponsive. (You can also see it takes bytes as input and converts to kB, which was the source of my earlier confusion in my comment.)

(Btw I was confused about PHP throwing an error for specifying more than one thread: it seems to me that it only does that if you use the sodium_password_hashX() functions. My prior experience has been using sodium_crypto_pwhash(), which is a more direct route to the Argon2id hashing function, and the best option if you need compatibility between Python- viz. Pysodium- and PHP.)

3
  • Thanks so much! I’ll have to look into that ReCAPTCHA issue more, though my form only takes username, email and password so as long as it can’t leak the password there shouldn’t be issues. As for password length, I actually do limit it client-side, don’t know why I didn’t do it server-side, so I’ll definitely do that. The Regex definitely works as I did quite a bit of testing with that, don’t worry. Finally, curious why ‘threads’ is an option if that’s the case but suppose I’ll lower that too.
    – hopperelec
    Oct 19, 2021 at 11:25
  • Can't seem to find anything about that ReCAPTCHA issue online so not sure what you're talking about there. I've not added the password length limit server-side. I've tested the Regex again and that definitely works. As for the threads thing, just looked at the Wiki again and noticed there's a warning saying 'Only available when PHP uses libargon2, not with libsodium implementation.' suggesting I can switch to libargon2 to get this feature. I'll have to do a bit more research into the differences between libargon2 and libsodium before I consider switching, though
    – hopperelec
    Oct 19, 2021 at 14:59
  • No problems! yep Libsodium is a broader library that offers a specific implementation of Argon2 as one of it's pw-kdf/ pw-hashing options - haven't used libargon2 before.
    – brynk
    Oct 19, 2021 at 19:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.