{"id":9287,"date":"2019-09-06T15:08:12","date_gmt":"2019-09-06T18:08:12","guid":{"rendered":"http:\/\/blog.plataformatec.com.br\/?p=9287"},"modified":"2019-09-09T10:09:08","modified_gmt":"2019-09-09T13:09:08","slug":"improve-confirmation-token-validation-in-devise-cve-2019-xxxx","status":"publish","type":"post","link":"http:\/\/blog.plataformatec.com.br\/2019\/09\/improve-confirmation-token-validation-in-devise-cve-2019-xxxx\/","title":{"rendered":"Improve confirmation token validation in Devise (CVE-2019-16109)"},"content":{"rendered":"\n

Devise version 4.7.1<\/code> 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.<\/p>\n\n\n\n

Description<\/h2>\n\n\n\n

We received a security report saying that it was possible to confirm records with a blank confirmation_token<\/code> parameter. In order words, hitting the following URL \/users\/confirmation?confirmation_token=<\/code> would successfully confirm a user instead of showing a validation error – e.g. Confirmation token can't be blank<\/code>.<\/p>\n\n\n\n

This only happens if there are records with a blank confirmation token in the database<\/strong>. This is because of the way the method find_first_by_auth_conditions<\/a><\/code> works (which is similar to ActiveRecord’s #find_by<\/code>). It is important to mention that we haven’t found a case where Devise sets confirmation_token<\/code> to an empty string.<\/p>\n\n\n\n

In summary, before version 4.7.1<\/code>, this is what would happen after hitting the confirmation endpoint with a blank confirmation_token<\/code>:<\/p>\n\n\n

Started GET \"\/users\/confirmation?confirmation_token=\"<\/span> for<\/span> ::1<\/span> at 2019<\/span>-09<\/span>-05<\/span> 10<\/span>:24<\/span>:29<\/span> -0300<\/span>\nProcessing by Devise::ConfirmationsController#show as HTML<\/span>\n Parameters: {\"confirmation_token\"<\/span>=>\"\"<\/span>}\n User Load (1.0<\/span>ms) SELECT \"users\"<\/span>.* FROM \"users\"<\/span> WHERE \"users\"<\/span>.\"confirmation_token\"<\/span> = $1<\/span> ORDER BY \"users\"<\/span>.\"id\"<\/span> ASC LIMIT $2<\/span> [[\"confirmation_token\"<\/span>, \"\"<\/span>], [\"LIMIT\"<\/span>, 1<\/span>]]<\/code><\/div>Code language:<\/span> PHP<\/span> (<\/span>php<\/span>)<\/span><\/small><\/pre>\n\n\n

Notice that the SQL query is trying to find users with confirmation_token = \"\"<\/code>.<\/p>\n\n\n\n

It’s also worth mentioning that only unconfirmed records<\/strong> – i.e. with confirmed_at: nil<\/code> in the database – would be confirmed in this case. For already confirmed users, a validation error (Email was already confirmed, please try signing in<\/code>) would be displayed.<\/p>\n\n\n\n

Possible implications<\/h2>\n\n\n\n

Considering that there are records with an empty confirmation_token<\/code> 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.<\/p>\n\n\n\n

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.<\/p>\n\n\n\n

For already confirmed records – i.e. with a confirmed_at<\/code> 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.<\/p>\n\n\n\n

Solution<\/h2>\n\n\n\n

The solution was to validate whether the confirmation_token<\/code> is empty before doing any query in the database. So now, when the same endpoint is hit, nothing happens:<\/p>\n\n\n

Processing by Devise::ConfirmationsController#show as HTML<\/span>\n Parameters: {\"confirmation_token\"<\/span>=>\"\"<\/span>}\nCompleted 200<\/span> OK in 371<\/span>ms (Views: 339.0<\/span>ms | ActiveRecord: 5.9<\/span>ms)<\/code><\/div>Code language:<\/span> PHP<\/span> (<\/span>php<\/span>)<\/span><\/small><\/pre>\n\n\n

And the end-user will see the validation error on the screen.<\/p>\n\n\n\n

Actions you might want to take<\/h2>\n\n\n\n

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:<\/p>\n\n\n

irb(main):001<\/span>:0<\/span>> User.where(confirmation_token: \"\"<\/span>)\n User Load (0.6<\/span>ms) SELECT \"users\"<\/span>.* FROM \"users\"<\/span> WHERE \"users\"<\/span>.\"confirmation_token\"<\/span> = $1<\/span> LIMIT $2<\/span> [[\"confirmation_token\"<\/span>, \"\"<\/span>], [\"LIMIT\"<\/span>, 11<\/span>]]\n=> #<ActiveRecord::Relation []><\/span><\/code><\/div>Code language:<\/span> PHP<\/span> (<\/span>php<\/span>)<\/span><\/small><\/pre>\n\n\n

If you get results out of this query, you might want to nullify them to avoid the confirmation by mistake:<\/p>\n\n\n

irb(main):002<\/span>:0<\/span>> User.where(confirmation_token: \"\"<\/span>).update(confirmation_token: nil)\n User Load (0.4<\/span>ms) SELECT \"users\"<\/span>.* FROM \"users\"<\/span> WHERE \"users\"<\/span>.\"confirmation_token\"<\/span> = $1<\/span> [[\"confirmation_token\"<\/span>, \"\"<\/span>]]\n (0.2<\/span>ms) BEGIN\n User Update (0.7<\/span>ms) UPDATE \"users\"<\/span> SET \"confirmation_token\"<\/span> = $1<\/span>, \"updated_at\"<\/span> = $2<\/span> WHERE \"users\"<\/span>.\"id\"<\/span> = $3<\/span> [[\"confirmation_token\"<\/span>, nil], [\"updated_at\"<\/span>, \"2019-09-05 14:03:20.857488\"<\/span>], [\"id\"<\/span>, 1<\/span>]]\n (1.2<\/span>ms) COMMIT<\/code><\/div>Code language:<\/span> JavaScript<\/span> (<\/span>javascript<\/span>)<\/span><\/small><\/pre>\n\n\n

Causes<\/h2>\n\n\n\n

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.<\/p>\n\n\n\n

If you find a case where this happens, please contact us at opensource@plataformatec.com.br<\/a> and we’ll look at it.<\/p>\n\n\n\n

Finally, we want to thank Anthony Mangano<\/a> for reporting this issue and helping with the solution.<\/p>\n","protected":false},"excerpt":{"rendered":"

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 … \u00bb<\/a><\/p>\n","protected":false},"author":54,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0,"footnotes":""},"categories":[1],"tags":[36,7],"aioseo_notices":[],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/9287"}],"collection":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/users\/54"}],"replies":[{"embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/comments?post=9287"}],"version-history":[{"count":10,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/9287\/revisions"}],"predecessor-version":[{"id":9300,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/posts\/9287\/revisions\/9300"}],"wp:attachment":[{"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/media?parent=9287"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/categories?post=9287"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/blog.plataformatec.com.br\/wp-json\/wp\/v2\/tags?post=9287"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}

See the pull request<\/a> with the solution for more information.<\/p>\n\n\n\n