Skip to content
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

prevent remember_token timing attacks #917

Merged

Conversation

gingerlime
Copy link
Contributor

  • see migrating from devise without kicking all sessions out? #916
  • similar to Prevent user enumeration by timing attacks #909
  • also see GHSA-hrqr-hxpp-chr3
    for an example of the type of attack that could be possible with an
    injectable cookie value
  • Rails provides signed cookies https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html
    since Rails 3 (??) which prevents tampering
  • using a signed cookie instead of a plain one, means the attacker cannot
    forge the cookie value, and therefore cannot perform timing attacks
    to find a valid token
  • another added value is that tampering with the cookie will
    not even hit the database
  • added a configuration parameter signed_cookie so this is optional
    and defaults to false for backwards compatibility
    (however, for better security, it might be better to issue a breaking
    change and default to true)
  • changed the add_cookies_to_headers method to use ActionDispatch /
    Rails' cookie-handling code to set the cookie
  • updated specs

* see thoughtbot#916
* similar to thoughtbot#909
* also see GHSA-hrqr-hxpp-chr3
  for an example of the type of attack that could be possible with an
  injectable cookie value
* Rails provides signed cookies https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html
  since Rails 3 (??) which prevents tampering
* using a signed cookie instead of a plain one, means the attacker cannot
  forge the cookie value, and therefore cannot perform timing attacks
  to find a valid token
* another added value is that tampering with the cookie will
  not even hit the database
* added a configuration parameter `signed_cookie` so this is optional
  and defaults to false for backwards compatibility
  (however, for better security, it might be better to issue a breaking
   change and default to true)
* changed the add_cookies_to_headers method to use ActionDispatch /
  Rails' cookie-handling code to set the cookie
* updated specs
@@ -14,15 +14,9 @@ def initialize(env)
# Called by {RackSession} to add the Clearance session cookie to a response.
#
# @return [void]
def add_cookie_to_headers(headers)
def add_cookie_to_headers(_headers)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just realized that this is being called from lib/clearance/rack_session.rb so we can actually remove the headers param which is no longer needed.

@gingerlime
Copy link
Contributor Author

Happy to refine the PR, but first want to check the project is open to this change in principle. I think it's important from a security standpoint, and I did my best not to break backwards compatibility. We're still not using clearance in production, but I tested it quickly on my dev environment, and it seems to work with signed cookies as well as without them.

@eebs
Copy link
Contributor

eebs commented Nov 30, 2020

Thanks for the PR @gingerlime. I agree that the change would make Clearance more secure and would like to find a way to move forward.

My main concerns are 1. Backwards compatibility and 2. Ease of transition. I'm wondering if it's possible to use some sort of fall back mechanism rather than an explicit configuration option. Would it make sense to always write the signed cookie when storing the remember token, and when reading the token we first look for a signed cookie and fall back to the regular cookie? Would this allow us to ship the change and as folks used the app it would migrate their cookies?

After some time we could remove the fallback. This would cause anyone that was signed in via a remember token to be signed out if they had not visited the app. This assumes that applications would upgrade to the version that introduced the fallback and then upgrade to the version that removed the fallback and relied solely on the encrypted cookie. If an application were to upgrade directly to the non-fallback version, all their users would be logged out. I think we could address this with a version bump if we felt uncomfortable with potentially logging users out.

What are your thoughts on this? Are there side effects I haven't considered? Thanks!

@gingerlime
Copy link
Contributor Author

Hi @eebs. Interesting. I like your thinking 👍

Yes, it's possible and even simplifies the code if we try the signed cookie and fall back to the plain one. It can also provide a smoother transition. This is similar to what we're planning to do when transitioning from devise to clearance :)

but ... (there's always a but)

  1. It's still not secure until you completely switch to signed cookies. i.e. your system is still vulnerable to timing attacks during the transition
  2. If you upgrade and then there's some (unrelated) problem, you cannot roll back to the previous version without kicking users out for mismatching tokens
  3. How do you plan to let users control the steps of the transition? e.g. for us, we don't need the transition, and prefer to switch to signed cookies from day one (we're not running clearance yet), other users might decide that they don't want to take the risk and switch to signed cookies within X days/weeks... So I think you're still looking at some kind of config to switch modes?

I would definitely see the benefit of having an option to transition cookies from plain to signed. So it's definitely worth looking at what it means to offer this transition option. I mentioned the code becomes simpler, but the tests will need quite a bit of work probably. However, I imagine that you would still want some way to control which option you use via config, rather than solely rely on versions.

Apologies, it's getting a bit late over here, so I might not have considered everything. I'm also still not familiar with the clearance codebase. Happy to continue this discussion and finding the best way forward.

@eebs
Copy link
Contributor

eebs commented Nov 30, 2020

Those are all great points, thanks for raising them. Perhaps we can find some compromise where we are secure by default, but for anyone that wishes to keep the old behavior, they could enable the configuration option. I'd like to think about this a bit more and gather some additional feedback. Happy to keep the discussion going in the meantime. 🤗

@gingerlime
Copy link
Contributor Author

gingerlime commented Nov 30, 2020

Yes, my initial thinking was to have a config defaulting to false (for backwards compatibility) and eventually defaulting to true (with the appropriate breaking changes warnings), and perhaps later on maybe removing the insecure option completely, but well into the future...

I didn't consider the transition problem... especially odd since the transition from devise brought me to the whole signed cookie thing in the first place ;-)

I just tested quickly and it's fairly simple to "migrate" insecure cookies with a prepend_before_filter like you suggested, e.g.

class ApplicationController < ActionController::Base
  before_action :require_login
  ...
  prepend_before_action :migrate_token

  def migrate_token
    # if cookie is already signed, do nothing
    return if cookies.signed["remember_token"]

    # if old (insecure) cookie exists
    if cookies["remember_token"].present?
      # and if we can find a valid user with it
      if user = User.find_by(:remember_token => cookies["remember_token"])
        sign_in(user)  # this will set the token to signed, if the config is set to true
      end
    end
  end

So then either users are advised to create this own migration on their own, or it can also be implemented within clearance (with another flag? or using a 3-state flag? e.g. true, false, migrate or something?)

Just throwing some ideas out there.

* the configuration value for signed_cookie can be set 3-ways:
  - `false` (default): backwards compatible, insecure
  - `:migrate`: converts unsigned cookies to signed ones
  - `true`: forces using signed cookies only
* these 3 options would allow users to transition to signed cookies
  and also the project to gradually change the default
* updated specs + added "validation" inside the configuration class
  to allow only those 3 values

context "when signed_cookie is set to an unexpected value" do
it "returns :migrate" do
expect {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/BlockDelimiters: Avoid using {...} for multi-line blocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eebs can this be configured in hound? I'm not sure where/how it's set or what configuration you use.

I personally think that with a DSL like rspec, it's more readable to use expect { something }.to eq(...) is much more readable than expect do something end.to eq(...)

Perhaps using Semantic or braces_for_chaining or adding expect to the list of BracesRequiredMethods ? see https://rubydoc.info/gems/rubocop/RuboCop/Cop/Style/BlockDelimiters

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback @gingerlime. We'll certainly consider that in the future but for now, let's go with the existing style guide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 of course it's your call. If you guys want to apply this style, then please go ahead. I'd rather keep my code contributions with what I personally consider to be readable, clear and concise.

@gingerlime
Copy link
Contributor Author

@eebs I pushed a commit that introduces a 3-way configuration for signed_cookie:

  • false (default, backwards compatible, insecure)
  • :migrate (still insecure, but allows users to migrate their unsigned cookies over a period of time)
  • true (forces signed cookies)

I think this gives users and the project the most flexibility. Thanks for pointing this out! and I hope it fits with your concept.

@gingerlime
Copy link
Contributor Author

@eebs it's been a while and I'm curious to figure out where the wind blows on this one 🌬️ :)

@eebs
Copy link
Contributor

eebs commented Dec 11, 2020

Hey @gingerlime sorry for the lack of communication, I haven't had time to follow up on this. I'm going to ask for some feedback from some folks internally (and would also love feedback from the rest of the community if you have thoughts!) and will get back to you soon. Thanks for your patience!

@mike-burns
Copy link
Contributor

The migration path taken here is excellent -- thank you for putting the effort into that. I think this is an overall improvement. LGTM.

Copy link
Contributor

@eebs eebs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your patience @gingerlime. I've left a few questions/comments but this is looking great.

Would you mind addressing the style issues noted by Hound as well?

Thanks!

when true, :migrate
cookies.signed[remember_token_cookie] = cookie_options(token)
when false
cookies[remember_token_cookie] = cookie_options(token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what the difference with this new implementation. We used to use Rack::Utils Is there any meaningful difference here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, Rack::Utils is probably much lower level. Rails might use it under the hood, or use some other way to add cookies to Rack. Since Clearance is geared towards Rails (exclusively?), then I think it makes sense to use the Rails' built-in helper for handling cookies rather than Rack::Utils... I could be wrong though, I'm not that familiar with Rails internals and didn't dig too deep into it.

if [true, false, :migrate].include? value
@signed_cookie = value
else
raise "unexpected signed_cookie; allowed: true/false/:migrate"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we could make this exception clearer? I'm concerned that if this were in a stack trace somewhere it wouldn't be clear to a user where it was coming from or how to fix it.

Would something like

"Clearance's signed_cookie configuration value is invalid. Valid values are true, false, or :migrate. Set this option via Clearance.configure in an initializer"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

lib/clearance/configuration.rb Outdated Show resolved Hide resolved
@gingerlime
Copy link
Contributor Author

@eebs Thanks for the feedback.

I made another change to remove the (now unnecessary) param to add_cookie_to_headers. I also applied your suggestions + fixed most of the styleguide issues. There were a couple that I personally don't feel comfortable with, but it's your project, so please go ahead and apply them as you see fit.

Btw, Travis is kinda dead as far as I can see? I've read on HN that they backed away from their promise to keep it free for open-source? perhaps you guys want to switch to github actions or something else?

@hound hound bot deleted a comment from gingerlime Dec 22, 2020
@hound hound bot deleted a comment from eebs Dec 22, 2020
@hound hound bot deleted a comment from mike-burns Dec 22, 2020
@hound hound bot deleted a comment from gingerlime Dec 22, 2020
@hound hound bot deleted a comment from gingerlime Dec 22, 2020
@eebs
Copy link
Contributor

eebs commented Jan 8, 2021

Thanks for the updates @gingerlime, I haven't forgotten about this PR but I've been busy recently. I'll work to get it in line with the style guide and merged.

I would be open to moving to GitHub Actions but don't have a lot of experience setting that up yet.

@gingerlime
Copy link
Contributor Author

@eebs I'm not that familiar with github actions either, but after a bit of reading I converted the travis.yml to github actions on #926 ... for some strange reason the rails 6.0 tests are failing, but I don't know why. I don't know how long travis stopped working, so perhaps the issue already existed before?

@gingerlime
Copy link
Contributor Author

Ah, looks like travis already failed with rails 6.0 anyway... and I don't think it's in any way related to this PR.

@gingerlime
Copy link
Contributor Author

Any updates on this one? can't say I'm super motivated to continue to contribute to this project considering how long it takes to get an security fix implemented.

We're running this branch live for a few weeks now with 1m+ registered users without any issues. Note however, we migrated from devise to clearance and went directly with the signed_cookie = true option.

@gnfisher
Copy link
Contributor

gnfisher commented Mar 5, 2021

Hello @gingerlime, sorry for the delay in getting back to you. Thank you very much for all your work on this! It looks great and ready to merge. 👍

@gnfisher gnfisher merged commit 2ff245a into thoughtbot:master Mar 5, 2021
MottiniMauro pushed a commit that referenced this pull request Mar 5, 2021
This commit introduces signed cookies into Clearance using the signed cookies
functionality provided by
[ActionDispatch](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html).
By using a signed cookie an attacker cannot forge the cookie value, and
therefore cannot perform timing attacks to find a valid token. See [this Rack
security
advisory](GHSA-hrqr-hxpp-chr3)
for an example of the type of attack that could be possible with an injectable
cookie value.

This change adds an optional configuration parameter `signed_cookie` which
defaults to false for backwards compatibility and does not use a signed cookie.
The other two options are `true` to use a signed cookie and `:migrate` which
converts unsigned cookies to signed ones and provides a safe transition path.
You can set this via `Clearance.configure` in an initializer:

```ruby
# ./config/initializers/clearance.rb

Clearance.configure do |config|
  # ...

  config.signed_cookie = :migrate

  # ...
end
```

This change also switched to using Rail's `ActionDispatch` for cookie handling
rather than `Rack::Utils`.

See related issues and pull requests:

* #916
* Similar to #909

Co-authored-by: Yoav Aner <[email protected]>
Co-authored-by: Eebs Kobeissi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants