-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Small security issue with :lockable #4981
Small security issue with :lockable #4981
Comments
Hey @cfeckardt, thanks for reporting this. Yeah, it definitely makes sense to do both operations in a row level transaction. We'll see how we can do this. Thanks! |
As reported in #4981, the method `#increment_failed_attempts` of `Devise::Models::Lockable` was not concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example: Browser1 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 Browser2 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 In the example above, `failed_attempts` should have been set to 3, but it will be set to 2. This commit handles this case by calling ActiveRecord's `#increment!` method, which will do this operation [atomically](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21).
As reported in #4981, the method `#increment_failed_attempts` of `Devise::Models::Lockable` was not concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example: Browser1 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 Browser2 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 In the example above, `failed_attempts` should have been set to 3, but it will be set to 2. This commit handles this case by calling ActiveRecord's `#increment!` method, which will do this operation [atomically](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21). Co-authored-by: Marcos Ferreira <marcos.ferreira@plataformatec.com.br>
As reported in #4981, the method `#increment_failed_attempts` of `Devise::Models::Lockable` was not concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example: Browser1 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 Browser2 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 In the example above, `failed_attempts` should have been set to 3, but it will be set to 2. This commit handles this case by calling ActiveRecord's `#increment!` method, which will do both steps at once, reading the value straight from the database. More info: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21 Co-authored-by: Marcos Ferreira <marcos.ferreira@plataformatec.com.br>
As reported in #4981, the method `#increment_failed_attempts` of `Devise::Models::Lockable` was not concurrency safe. The increment operation was being done in two steps: first the value was read from the database, and then incremented by 1. This may result in wrong values if two requests try to update the value concurrently. For example: ``` Browser1 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 Browser2 -------> Read `failed_attempts` from DB (1) -------> Increment `failed_attempts` to 2 ``` In the example above, `failed_attempts` should have been set to 3, but it will be set to 2. This commit handles this case by calling `ActiveRecord::CounterCache.increment_counter` method, which will do both steps at once, reading the value straight from the database. This commit also adds a `ActiveRecord::AttributeMethods::Dirty#reload` call to ensure that the application gets the updated value - i.e. that other request might have updated. Although this does not ensure that the value is in fact the most recent one - other request could've updated it after the `reload` call - it seems good enough for this implementation. Even if a request does not locks the account because it has a stale value, the next one - that updated that value - will do it. That's why we decided not to use a pessimistic lock here. Closes #4981.
Has a CVE been assigned to this TOCTOU issue? |
@reedloden Not yet. I want to apologize for taking so long to reply on this but I knew nothing about CVEs and had to do some research on this topic before I was able to do it. |
@cfeckardt I'd also like to add that for security issues it's better to report to us directly via the opensource@plataformatec.com.br email (like we mention in our contribution guide). This way we don't leak the vulnerability before we can release a fix. At first, I thought this issue wasn't a security breach since the attacker still has to succeed on a brute force attack in order to exploit this. After talking to other people and analyzing deeply, it really is a vulnerability since people are using the Now the issue is public for quite some time, so I guess it's ok to leave it here. Just to keep everyone on the same page, this was fixed on v4.6.0. Thanks, everyone! |
I prepped rubysec/ruby-advisory-db#375 for this. Just need the CVE ID. @cfeckardt let me know if you run into issues getting a CVE ID. I can make something happen if needed. |
@reedloden Thanks, I've accepted the terms last Sunday and I'm waiting for a response. I'll let you know if I don't get any. |
@tegon been 19 days... any updates? |
I asked Kurt Seifried yesterday about it and I'm waiting for a response. This was addressed in this pull request that later got closed, so I'm not sure if it's going to be included in a new PR or not. |
@reedloden Kurt said that the DWF has been shut down 😞 |
@tegon Huh, hadn't heard that. Sad to hear... In any case, this has been assigned CVE-2019-5421. I'll get this updated at MITRE shortly. |
@reedloden Thank you! |
Any plans to make a patch release for 4.4.3? |
@dougo There are no plans yet but can we can discuss it. Are you having complications updating to 4.6 - e.g. something is broken for you? |
Yep, the "Set encrypted_password to nil when password is set to nil" bugfix made a bunch of our tests fail when I upgraded (I guess we were relying on it being empty string, and we have a non-null constraint in the db). Probably easy to fix, but not the sort of yak-shaving I wanted to do while addressing a CVE... |
There was a security vulnerability with previous version heartcombo/devise#4981
There was a security vulnerability with previous version heartcombo/devise#4981
Updates the requirements on [devise](https://github.com/plataformatec/devise) to permit the latest version. - [Release notes](https://github.com/plataformatec/devise/releases) - [Changelog](https://github.com/plataformatec/devise/blob/master/CHANGELOG.md) - [Commits](heartcombo/devise@v4.4.0...v4.6.1) Signed-off-by: dependabot[bot] <support@dependabot.com>
There was a security vulnerability with previous version heartcombo/devise#4981
CVE-2019-5421 More information moderate severity Vulnerable versions: < 4.6.0 Patched version: 4.6.0 Devise ruby gem before 4.6.0 when the lockable module is used is vulnerable to a time-of-check time-of-use (TOCTOU) race condition due to increment_failed_attempts within the Devise::Models::Lockable class not being concurrency safe. heartcombo/devise#4981
There was a security vulnerability with previous version heartcombo/devise#4981
There was a security vulnerability with previous version heartcombo/devise#4981
There was a security vulnerability with previous version heartcombo/devise#4981
Bootstrap version < 4.3.1 Ref: https://nvd.nist.gov/vuln/detail/CVE-2019-8331 Devise version < 4.6.0 Ref: heartcombo/devise#4981
Bootstrap version < 4.3.1 Ref: https://nvd.nist.gov/vuln/detail/CVE-2019-8331 Devise version < 4.6.0 Ref: heartcombo/devise#4981
Fixes security issues: - https://nvd.nist.gov/vuln/detail/CVE-2019-16109 - heartcombo/devise#4981
Patch vulnerabilities: Devise and Nokogiri Patches three security issues: - https://nvd.nist.gov/vuln/detail/CVE-2019-16109 - heartcombo/devise#4981 - https://nvd.nist.gov/vuln/detail/CVE-2019-5477
Environment
Current behavior
Pentesters found an issue where our users did not lock (using :lockable), despite running many many attempts to brute force the password.
To reproduce, try to login many times at exactly the same time (where your model is :lockable).
100 attempts within 1 milliseconds of each other will not increment the failed_attempts attribute on your user 100 times on a busy or slow database. They will most likely be incremented by approximately
100 % num_threads
where num_threads is the number of threads your server is configured to.This is a test that describes the behaviour that I would reasonably expect:
This test will fail in devise 4.5.0
Expected behavior
The issue arises because we read and set failed_attempts in two steps, instead of in one database transaction.
Below is an excerpt of the method from
lockable.rb
, my comments added:Our workaround we use is something along the lines of:
which passes the test above. This solution is postgres specific (and also assumes integer ids) so may not be suitable for devise, but the idea stands. Reading and writing from this attribute needs to happen on at least a row level transaction in the database.
The text was updated successfully, but these errors were encountered: