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

Reset push rate limit on successful gem push #2311

Merged
merged 4 commits into from May 23, 2020

Conversation

sonalkr132
Copy link
Member

We have rate limit with exp bacioff on endpoints with mfa to mitigate brute force of the otp code
https://security.stackexchange.com/questions/185905/maximum-tries-for-2fa-code
Check #2078 for more info on the attack.

Ideally, we only need to throttle 401 unauthorized requests to mitigate this, however rack-attack doesn't have option of rate limit on response status. resetting rate limit cache on successful push is the closest alternative (smiliar usecase).

Exp rate limit was removed from push as we had used incorrect back off period and users were seeing unreasonable backoff time. This has since been fixed, max back off period is now of 11 days.

We have rate limit with exp bacioff on endpoints with mfa to mitigate
brute force of the otp code
https://security.stackexchange.com/questions/185905/maximum-tries-for-2fa-code

Ideally, we only need to throttle 401 unauthorized requests to mitigate
this, however rack-attack doesn't have option of rate limit on response
status. resetting rate limit cache on successful push is the closest
alternative (smiliar
[usecase](https://github.com/gitlabhq/gitlabhq/blob/4529c19950e412f0461910585414f8633d3b1b18/lib/gitlab/auth/ip_rate_limiter.rb#L17)).

Exp rate limit was removed from push as we had used
incorrect back off period and users were seeing unreasonable backoff
time. This has since been fixed, max back off period is now of 11 days.
@sonalkr132 sonalkr132 requested a review from simi April 19, 2020 10:10
@sonalkr132
Copy link
Member Author

I am hoping to avoid #2081 and #2263, which were resolved by #2268. However, we do need exponential backoff on mfa endpoints.

Please let me know something is not clear.

@simi
Copy link
Member

simi commented Apr 19, 2020

Ideally, we only need to throttle 401 unauthorized requests to mitigate this, however rack-attack doesn't have option of rate limit on response status. resetting rate limit cache on successful push is the closest alternative (smiliar usecase).

Not sure how hurry are you in here @sonalkr132, but alternativelly I can take a look if it would not be possible to contribute this to rack-test actually. Would that work in here?

Rack::Attack.throttle("requests by ip", limit: 5, period: 2, status: 401) do |request|
  request.ip
end

@@ -105,6 +106,7 @@ def after_write
Delayed::Job.enqueue Indexer.new, priority: PRIORITIES[:push]
rubygem.delay.index_document
GemCachePurger.call(rubygem.name)
RackAttackReset.gem_push_backoff(@remote_ip)
Copy link
Member

@simi simi Apr 19, 2020

Choose a reason for hiding this comment

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

Should we do that only when @remote_ip.present??

@sonalkr132
Copy link
Member Author

Not sure how hurry are you in here

This attack is only possible if you already know the api key or the password. Even after that success of the brute force (after continuous attempt for days) is very probabilistic. So, not urgent.

I can take a look if it would not be possible to contribute this to rack-test actually

They have declined the request to limit on response in past rack/rack-attack#216

should "reset gem push rate limit rack attack key" do
Rack::Attack::EXP_BACKOFF_LEVELS.each do |level|
push_exp_throttle_level_key = "#{Rack::Attack::PUSH_EXP_THROTTLE_KEY}/#{level}:#{@ip_address}"
assert_equal 1, Rack::Attack.cache.count(push_exp_throttle_level_key, exp_base_limit_period**level)
Copy link
Member Author

Choose a reason for hiding this comment

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

Calling Rack::Attack.cache.count increments the key, hence this asserts for 1, instead of 0. I couldn't get read cache to work.. something about key prefix.

Copy link
Member

Choose a reason for hiding this comment

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

I'll take a look. 👀

Copy link
Member

Choose a reason for hiding this comment

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

yes, you need to specify "time" part manually for read

this code works (and fails if I comment out reset in rack_attack_reset.rb)

should "reset gem push rate limit rack attack key" do
  Rack::Attack::EXP_BACKOFF_LEVELS.each do |level|
    push_exp_throttle_level_key = "#{Rack::Attack::PUSH_EXP_THROTTLE_KEY}/#{level}:#{@ip_address}"
    period = exp_base_limit_period**level
    key = "#{Time.now.to_i / period}:#{push_exp_throttle_level_key}"

    assert_equal nil, Rack::Attack.cache.read(key)
  end
end

got inspired at https://github.com/kickstarter/rack-attack/blob/580368faddcb563b55ad2ec4aa4cd05aae845547/spec/rack_attack_throttle_spec.rb#L72-L78

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks simi. Updated to this.

#{Time.now.to_i / period}

Doesn't this mean if period was much smaller 1-2 seconds, key could change between setup and assert?

Copy link
Member

Choose a reason for hiding this comment

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

It can happen with any period actually. It should be safer to wrap those specs with freeze_time. 👍

https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-freeze_time

Copy link
Member Author

Choose a reason for hiding this comment

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

The idea of the key changing between the time key is set (in rack_attack.rb) and reset (in pusher.rb) did cross my mind but I assumed their reset_count implementation must be correct. Looking at this more closing after your comment, I don't think it is. Consider following:

# time during `count` in rack_attack.rb
> Time.now.to_i 
 => 1587326999
> Time.now.to_i / 100 
 => 1587326

# churn code, query pg, upload to s3. time during `reset_count`  in pusher.rb
> Time.now.to_i 
=> 1587327001
> Time.now.to_i / 100 
=> 1587327

To reiterate, key_and_expiry gets called from both count and reset_count but there is no grantee that Time.now will be the same.

I suppose similar to their workaround for key expiry rack/rack-attack#85 we should purge be purging two keys (rack::attack:1587326:.. and rack::attack:1587327:..).

@sonalkr132
Copy link
Member Author

@simi updated this to reset two keys. check last commit message or this comment

we need to delete two keys to ensure push rate limit is reset.
time_counter (`(Time.now.to_i / period).to_i`) used in rack attack key
can change between the time the key was set and the time when we reset/delete it.

time during `count` in rack_attack.rb
> Time.now.to_i
 => 1587326999
> Time.now.to_i / 100
 => 1587326

churn code, query pg, upload to s3. time during `gem_push_backoff` in pusher.rb
> Time.now.to_i
=> 1587327001
> Time.now.to_i / 100
=> 1587327
@sonalkr132 sonalkr132 merged commit ecb1bd1 into rubygems:master May 23, 2020
@sonalkr132 sonalkr132 deleted the mfa-exp-backoff branch May 23, 2020 18:26
@rubygems-org-shipit rubygems-org-shipit bot temporarily deployed to staging May 25, 2020 08:27 Inactive
sonalkr132 added a commit to sonalkr132/rubygems.org that referenced this pull request May 25, 2020
tests

Fixes: NameError: undefined local variable or method
`expected_retry_after' for #<RackAttackTest:0x000000000c99e1f0>

https://travis-ci.org/github/rubygems/rubygems.org/jobs/690929419

test failing in the build was added in rubygems#2311
@dwradcliffe dwradcliffe temporarily deployed to production June 4, 2020 12:32 Inactive
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.

None yet

3 participants