pallets-eco / flask-security

Quick and simple security for Flask applications
MIT License
639 stars 154 forks source link

Add support for anti-brute force #207

Open jwag956 opened 4 years ago

jwag956 commented 4 years ago

OWASP https://github.com/OWASP/ASVS/blob/master/4.0/en/0x11-V2-Authentication.md#v21-password-security-requirements 2.2.1 talks about brute force mitigation:

Verify that no more than 100 failed attempts per hour is possible on a single account.

This can probably be implemented as part of tracking. Could also add slowing down responses (but of course attacker can send multiple requests that would be handled by different threads).

Some good background: https://security.stackexchange.com/questions/85435/silently-limiting-login-attempts?rq=1 In particular - watch out for telling people an account is locked out since that relays info that the account/username is valid! (send email/SMS instead).

And this one: https://security.stackexchange.com/questions/74211/what-is-the-difference-between-login-throttling-and-temporary-account-lockout

NIST 5.2.2 - Unless otherwise specified in the description of a given authenticator, the verifier SHALL limit consecutive failed authentication attempts on a single account to no more than 100. and then... When the subscriber successfully authenticates, the verifier SHOULD disregard any previous failed attempts for that user from the same IP address.

What is confusing in the NIST verbiage is the first sentence talks about 'account' the second talks about IP addresses.

TaaviE commented 4 years ago

IMHO this should be primarily implemented by things like OSSEC and fail2ban, those tools can also be more effective at deploying countermeasures. Providing a paragraph in documentation that one of them should be set up would be quite good already.

callejerog commented 4 years ago

I solved this way:

In my user_model: wrong_pwd_counter = db.Column(db.Integer) and ` class CustomLoginForm(LoginForm):

def validate(self):
    response = True
    if not super(LoginForm, self).validate():
        response = False
    self.user = _security.datastore.get_user(self.email.data)
    if response and not self.user.is_active:
        self.email.errors.append(get_message("DISABLED_ACCOUNT")[0])
        response =  False
    if response and self.user is None:
        self.email.errors.append(get_message("USER_DOES_NOT_EXIST")[0])
        hash_password(self.password.data)
        response = False
    if response and not self.user.password:
        self.password.errors.append(get_message("PASSWORD_NOT_SET")[0])
        hash_password(self.password.data)
        response = False
    if response and not self.user.verify_and_update_password(self.password.data):
        self.password.errors.append(get_message("INVALID_PASSWORD")[0])
        response = False
    if response and requires_confirmation(self.user):
        self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0])
        response = False
    if not response and 'password' in self.errors.keys():
        self.user.wrong_pwd_counter += 1
        if self.user.wrong_pwd_counter >= Config.max_wrong_pwd:
            self.user.active = False
            notify_account_locked(self.user.email)
        self.user.save()
    if response:
        self.user.wrong_pwd_counter = 0
        self.user.save()
        if not self.user.can_login:
            self.password.errors.append('Cannot access. Call assistance')
            response = False
    return response

`

illume commented 4 years ago

Would be good if it had something for this enabled by default.

I solved this partially with flask limit.

Also using real time black hole lists, and a CDN in front that has it's own security lists/WAF, and such... But still they get through. They are very, very sneaky (hundreds of proxy IPs, and using up all the attempts per ip for example).

However, it depends on your user base. If your organization has 1000s of people on one ip address, then these blocks fail. With industrial NAT becoming more common in some parts of the world and with ipv4s having run out - this will only get worse. Still, it's probably worth enabling something by default.

(ps. thanks lots for maintaining this!)