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

LG-12712 Use phone fingerprint as rate limit key for phone confirmation #10459

Closed
wants to merge 27 commits into from

Conversation

kevinsmaster5
Copy link
Contributor

@kevinsmaster5 kevinsmaster5 commented Apr 18, 2024

🎫 Ticket

Link to the relevant ticket:
LG-12712

🛠 Summary of changes

Adds a method to PhoneSetupController that gets called before create and checks the submitted phone as an encoded fingerprint against a RateLimit. Specify limit to 20 submissions in a 10 minute window and not on a per-user basis but rather on a per phone basis.

📜 Testing Plan

(in your local application.yml set phone_submissions_per_fingerprint_limit: 5 for easier testing)

  • Log in at http://localhost:3000
  • Attempt to add a phone and when OTP page shows click 'Use another phone number' and repeat 5x with the same number
  • Observe you are sent to your account page with a flash warning ' you tried too many times ... ' and a 10 minute cooldown
  • Log out, log in as another user
  • Attempt to add the same phone number as above and receive the same warning and lockout to account page

)
submission_rate_limiter.increment!

lock_out_user if submission_rate_limiter.maxed?
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this could be potentially problematic in the case where I maliciously submit the same number a bunch of times, and then you come along within the next N timeframe and submit it for the first time (that you're aware of), and the rate limiter locks you out.

Comment on lines 144 to 147
submission_rate_limiter ||= RateLimiter.new(
target: @phone,
rate_limit_type: :phone_submissions,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's overlap between this and the OtpRateLimiter here, which uses the phone number hash as the discriminator in the same way. Is it worth looking at updating that instead of creating a somewhat duplicative rate limiter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was looking at OtpRateLimiter initially but since I only so far recreate the phone_fingerprint method. I thought maybe I'd have to do something with the otp_findtime and otp_maxretry_times functions since they are specific to how it is originally applied, but maybe they wouldn't have had any impact on using it here.
It just seemed to be less impact calling Pii::Fingerprinter.fingerprint(... than revising OtpRateLimiter.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess my suggestion is more along the lines of whether we should update the configuration values used in OtpRateLimiter rather than add a very similar additional rate limit.

Right now, otp_findtime and otp_maxretry_times are used for both confirmed and unconfirmed phone numbers in OtpRateLimiter, but I think it's worth considering splitting that to have separate configurations for confirmed/unconfirmed phone numbers over the approach here.

@kevinsmaster5 kevinsmaster5 marked this pull request as ready for review April 22, 2024 14:12
@kevinsmaster5 kevinsmaster5 requested a review from a team April 22, 2024 17:46
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
config/application.yml.default Outdated Show resolved Hide resolved
@kevinsmaster5 kevinsmaster5 marked this pull request as draft April 22, 2024 18:45
@kevinsmaster5 kevinsmaster5 marked this pull request as ready for review April 24, 2024 13:26
@kevinsmaster5 kevinsmaster5 requested a review from a team April 25, 2024 18:09
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

I feel like the reuse of OtpRateLimiter is nice insofar as being able to reuse some of the logic around creating a rate-limit key with the phone fingerprint, but that's really all the value I see from that class. I don't think we should inherit the same behaviors of locking out the user or logging them out. I'd almost rather we just extracted a PhoneFingerprinter service class and avoid OtpRateLimiter altogether.

app/services/otp_rate_limiter.rb Outdated Show resolved Hide resolved
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
spec/controllers/users/phone_setup_controller_spec.rb Outdated Show resolved Hide resolved
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
app/services/rate_limiter.rb Outdated Show resolved Hide resolved
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
except: :seconds,
),
)
redirect_to account_path
Copy link
Member

Choose a reason for hiding this comment

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

I really don't think we should redirect the user to the account page, since that may throw them off course if they had intended to return to the service provider.

This kind of validation should probably be part of NewPhoneForm, to keep the controller lean and so that it can take advantage of the existing unsuccessful submission handling in the create method above.

That being said, create doesn't handle error messages, or at least it didn't as of the current branch point of this pull request. This was changed in #10454 to support error messages for reCAPTCHA. I could maybe imagine the code for first_error_message being updated to support multiple keys so that we could surface validation errors for either reCAPTCHA or rate-limiting.

flash.now[:error] = result.first_error_message(:rate_limited, :recaptcha_token)

At the very least, I'd think we should redirect back to the phone setup form so the error message is shown on the same page, not redirect the user away..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense to let the limiter do the work. I have changed that and moved the validation to the form. I'm not 100% sure how I executed the error message since it doesn't use flash but it looks like it's working.

app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
app/controllers/users/phone_setup_controller.rb Outdated Show resolved Hide resolved
app/forms/new_phone_form.rb Outdated Show resolved Hide resolved
@kevinsmaster5 kevinsmaster5 force-pushed the kmas-lg-12712-phone-fingerprint-rate-limit branch from b4ab5a3 to 07a7ceb Compare April 30, 2024 18:37
@kevinsmaster5 kevinsmaster5 force-pushed the kmas-lg-12712-phone-fingerprint-rate-limit branch from 22acfd5 to 53b187c Compare May 1, 2024 12:15
app/services/form_response.rb Outdated Show resolved Hide resolved
Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Can we add a feature spec which validates that the alert banner is visible to the user after they're rate-limited?

(I believe this will help surface a bug in the current implementation)

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

LGTM 👍

end
it 'displays an error banner' do
sign_in_before_2fa(@user)
allow(IdentityConfig.store).to receive(:otp_delivery_blocklist_maxretry).and_return(999)
Copy link
Member

Choose a reason for hiding this comment

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

Is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not at all, apparently. It would be useful for hammering at the phone submissions limiter and not wanting to hit that other limit it seems. Isn't a problem here. Removed.

allow(IdentityConfig.store).to receive(:otp_delivery_blocklist_maxretry).and_return(999)
allow(IdentityConfig.store).to receive(
:phone_submissions_per_fingerprint_max_attempts_window_in_minutes,
).and_return(10)
Copy link
Member

Choose a reason for hiding this comment

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

We could make the test run a little faster if we reduced this a bit so it's not looping as many times in the loop that follows.

Suggested change
).and_return(10)
).and_return(2)


def validate_phone_submission_limit
fingerprint = Pii::Fingerprinter.fingerprint(Phonelib.parse(phone).e164.to_s)
@submission_rate_limiter ||= RateLimiter.new(
Copy link
Member

Choose a reason for hiding this comment

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

If we're not using this anywhere outside the scope of this method, I don't think we need to assign it as an instance variable.

Suggested change
@submission_rate_limiter ||= RateLimiter.new(
submission_rate_limiter ||= RateLimiter.new(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense. 👍

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

Temporarily submitting a blocking review, I'm looking at whether there's some redundancy here with existing rate-limiting.

@aduth
Copy link
Member

aduth commented May 3, 2024

Unfortunately, I think this behavior may already be implemented, stemming back to @mitchellhenke's comment in #10459 (comment) . It's a little non-obvious, especially since it happens through the TwoFactorAuthenticationController controller, which is typically only responsible for authentication, not MFA setup. But in the case of phone setup, the code confirmation also goes through that controller.

There's an existing feature spec here which appears to verify that this behavior is expected to be enforced:

it 'limits the number of times a code can be sent to a phone across accounts' do
visit_otp_confirmation(delivery_method)
max_otp_sends.times do
click_on t('links.two_factor_authentication.send_another_code')
end
Capybara.reset_session!
visit root_path
sign_up_and_set_password
select_2fa_option(:phone)
fill_in :new_phone_form_phone, with: phone
click_send_one_time_code
expect(page).to have_content(t('two_factor_authentication.max_otp_requests_reached'))
expect_user_to_be_rate_limitted
expect_rate_limitting_to_expire
end

The actual numbers of the rate limit may be different from what we had come up with for this work, but I think we'd be okay with slight differences from what we'd planned.

Can you take a look and confirm if the behavior we were seeking to implement here is already handled via OtpRateLimiter?

@kevinsmaster5
Copy link
Contributor Author

kevinsmaster5 commented May 3, 2024

Can you take a look and confirm if the behavior we were seeking to implement here is already handled via OtpRateLimiter?

Affirmative. It does in fact apply rate limiting for phone fingerprint regardless of the account used.
The difference being as you noted OtpRateLimiter checks during 2FA rather than at phone setup as in this PR. Also a distinction between that and what I've been working on is the user doesn't get signed out and their 2FA locked where in the OtpRateLimiter it does. Which ironically could lead to a scenario cautioned against in this comment and in our slack discussion.

@aduth aduth deleted the kmas-lg-12712-phone-fingerprint-rate-limit branch May 15, 2024 18:13
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