From e2b5bdfa8a5676f565895807ce106f6f2b42a202 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 18 May 2021 14:23:50 -0500 Subject: [PATCH] Block repeated invite code tries by customer id So it's not as trivial to brute-force the space and find an open one. Limit is 10 tries per hour. --- lib/registration.rb | 21 +++++++++-- test/test_registration.rb | 76 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index 487636d..8935505 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -313,16 +313,33 @@ class Registration def write COMMAND_MANAGER.write(@reply).then do |iq| - verify(iq.form.field("code")&.value&.to_s).then { + guard_too_many_tries.then { + verify(iq.form.field("code")&.value&.to_s) + }.then { Finish.new(iq, @customer, @tel) }.catch_only(Invalid) { |e| - InviteCode.new(iq, @customer, @tel, error: e.message) + invalid_code(iq, e) }.then(&:write) end end protected + def guard_too_many_tries + REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t| + raise Invalid, "Too many wrong attempts" if t > 10 + end + end + + def invalid_code(iq, e) + EMPromise.all([ + REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do + REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60) + end, + InviteCode.new(iq, @customer, @tel, error: e.message) + ]).then(&:last) + end + def customer_id @customer.customer_id end diff --git a/test/test_registration.rb b/test/test_registration.rb index 4f079d9..4338654 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -342,6 +342,8 @@ class RegistrationTest < Minitest::Test class InviteCodeTest < Minitest::Test Registration::Payment::InviteCode::DB = Minitest::Mock.new + Registration::Payment::InviteCode::REDIS = + Minitest::Mock.new Registration::Payment::InviteCode::COMMAND_MANAGER = Minitest::Mock.new Registration::Payment::InviteCode::Finish = @@ -349,6 +351,11 @@ class RegistrationTest < Minitest::Test def test_write customer = Customer.new("test", plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :get, + EMPromise.resolve(0), + ["jmp_invite_tries-test"] + ) Registration::Payment::InviteCode::COMMAND_MANAGER.expect( :write, EMPromise.resolve( @@ -380,12 +387,18 @@ class RegistrationTest < Minitest::Test ).write.sync Registration::Payment::InviteCode::COMMAND_MANAGER.verify Registration::Payment::InviteCode::DB.verify + Registration::Payment::InviteCode::REDIS.verify Registration::Payment::InviteCode::Finish.verify end em :test_write def test_write_bad_code customer = Customer.new("test", plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :get, + EMPromise.resolve(0), + ["jmp_invite_tries-test"] + ) Registration::Payment::InviteCode::COMMAND_MANAGER.expect( :write, EMPromise.resolve( @@ -401,6 +414,16 @@ class RegistrationTest < Minitest::Test Registration::Payment::InviteCode::DB.expect(:transaction, []) do raise Registration::Payment::InviteCode::Invalid, "wut" end + Registration::Payment::InviteCode::REDIS.expect( + :incr, + EMPromise.resolve(nil), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::REDIS.expect( + :expire, + EMPromise.resolve(nil), + ["jmp_invite_tries-test", 60 * 60] + ) Registration::Payment::InviteCode::COMMAND_MANAGER.expect( :write, EMPromise.reject(Promise::Error.new), @@ -420,9 +443,60 @@ class RegistrationTest < Minitest::Test end Registration::Payment::InviteCode::COMMAND_MANAGER.verify Registration::Payment::InviteCode::DB.verify - Registration::Payment::InviteCode::Finish.verify + Registration::Payment::InviteCode::REDIS.verify end em :test_write_bad_code + + def test_write_bad_code_over_limit + customer = Customer.new("test", plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :get, + EMPromise.resolve(11), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve( + Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [{ var: "code", value: "abc" }] + } + ), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_nil reply.form.instructions + end] + ) + Registration::Payment::InviteCode::REDIS.expect( + :incr, + EMPromise.resolve(nil), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::REDIS.expect( + :expire, + EMPromise.resolve(nil), + ["jmp_invite_tries-test", 60 * 60] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.reject(Promise::Error.new), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_equal "Too many wrong attempts", reply.form.instructions + end] + ) + iq = Blather::Stanza::Iq::Command.new + iq.from = "test@example.com" + assert_raises Promise::Error do + Registration::Payment::InviteCode.new( + iq, + customer, + "+15555550000" + ).write.sync + end + Registration::Payment::InviteCode::COMMAND_MANAGER.verify + Registration::Payment::InviteCode::REDIS.verify + end + em :test_write_bad_code_over_limit end end -- 2.38.5