Devise version 4.7.1
was released with a fix for an edge case that could confirm accounts by mistake. We’ll explain now in details what is the issue, how it was fixed and which actions you might want to take in your applications.
Description
We received a security report saying that it was possible to confirm records with a blank confirmation_token
parameter. In order words, hitting the following URL /users/confirmation?confirmation_token=
would successfully confirm a user instead of showing a validation error – e.g. Confirmation token can't be blank
.
This only happens if there are records with a blank confirmation token in the database. This is because of the way the method find_first_by_auth_conditions
works (which is similar to ActiveRecord’s #find_by
). It is important to mention that we haven’t found a case where Devise sets confirmation_token
to an empty string.
In summary, before version 4.7.1
, this is what would happen after hitting the confirmation endpoint with a blank confirmation_token
:
Started GET "/users/confirmation?confirmation_token=" for ::1 at 2019-09-05 10:24:29 -0300
Processing by Devise::ConfirmationsController#show as HTML
Parameters: {"confirmation_token"=>""}
User Load (1.0ms) SELECT "users".* FROM "users" WHERE "users"."confirmation_token" = $1 ORDER BY "users"."id" ASC LIMIT $2 [["confirmation_token", ""], ["LIMIT", 1]]
Code language: PHP (php)
Notice that the SQL query is trying to find users with confirmation_token = ""
.
It’s also worth mentioning that only unconfirmed records – i.e. with confirmed_at: nil
in the database – would be confirmed in this case. For already confirmed users, a validation error (Email was already confirmed, please try signing in
) would be displayed.
Possible implications
Considering that there are records with an empty confirmation_token
in the database, a request sending a blank parameter would confirm the first record found in the database. This means that someone’s account would be confirmed by mistake.
A more sensible scenario is for applications that automatically sign in accounts after confirmation. That would not only confirm someone’s account but also give an attacker access to it. Although this feature is not included in Devise, we know that some applications might have it.
For already confirmed records – i.e. with a confirmed_at
date in the database – the validation error would be displayed, but their email would be leaked to the end user inside the form’s input.
Solution
The solution was to validate whether the confirmation_token
is empty before doing any query in the database. So now, when the same endpoint is hit, nothing happens:
Processing by Devise::ConfirmationsController#show as HTML
Parameters: {"confirmation_token"=>""}
Completed 200 OK in 371ms (Views: 339.0ms | ActiveRecord: 5.9ms)
Code language: PHP (php)
And the end-user will see the validation error on the screen.
See the pull request with the solution for more information.
Actions you might want to take
Aside from updating Devise, you might also want to check whether you have records in that state in your application. You can do that with a query like this one:
irb(main):001:0> User.where(confirmation_token: "")
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."confirmation_token" = $1 LIMIT $2 [["confirmation_token", ""], ["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
Code language: PHP (php)
If you get results out of this query, you might want to nullify them to avoid the confirmation by mistake:
irb(main):002:0> User.where(confirmation_token: "").update(confirmation_token: nil)
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."confirmation_token" = $1 [["confirmation_token", ""]]
(0.2ms) BEGIN
User Update (0.7ms) UPDATE "users" SET "confirmation_token" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["confirmation_token", nil], ["updated_at", "2019-09-05 14:03:20.857488"], ["id", 1]]
(1.2ms) COMMIT
Code language: JavaScript (javascript)
Causes
Although we received a report where someone worked in an application that had records with a blank confirmation token in the database, no code or use case was found inside Devise that would make records end up in that state.
If you find a case where this happens, please contact us at opensource@plataformatec.com.br and we’ll look at it.
Finally, we want to thank Anthony Mangano for reporting this issue and helping with the solution.