102 lines
3.3 KiB
Ruby
102 lines
3.3 KiB
Ruby
require 'minitest/autorun'
|
|
require 'prism'
|
|
require 'support'
|
|
|
|
class MiddlewareRateLimitingTest < Minitest::Test
|
|
include TestSupport
|
|
|
|
def setup
|
|
Prism::RateLimiter.reset!
|
|
@downstream_calls = 0
|
|
downstream = lambda do |_env|
|
|
@downstream_calls += 1
|
|
[200, {}, ['ok']]
|
|
end
|
|
@app = Prism::Middleware::RateLimiting.new(downstream)
|
|
end
|
|
|
|
def teardown
|
|
Prism::RateLimiter.reset!
|
|
end
|
|
|
|
def request(ip: '1.1.1.1', path: '/login')
|
|
env = env_for('POST', path)
|
|
env['REMOTE_ADDR'] = ip
|
|
@app.call(env)
|
|
end
|
|
|
|
def test_requests_under_the_limit_pass_through
|
|
Prism::RateLimiter.throttle('logins/ip', limit: 2, period: 60) { |req| req.env['REMOTE_ADDR'] }
|
|
|
|
2.times do
|
|
status, = request
|
|
assert_equal 200, status
|
|
end
|
|
assert_equal 2, @downstream_calls
|
|
end
|
|
|
|
def test_request_over_the_limit_is_throttled_with_retry_after
|
|
Prism::RateLimiter.throttle('logins/ip', limit: 2, period: 60) { |req| req.env['REMOTE_ADDR'] }
|
|
|
|
2.times { request }
|
|
status, headers, body = request
|
|
|
|
assert_equal 429, status
|
|
assert_equal '60', headers['Retry-After']
|
|
assert_includes body.join, 'Too Many Requests'
|
|
assert_equal 2, @downstream_calls, 'the throttled request must not reach the app'
|
|
end
|
|
|
|
def test_throttle_keys_are_independent_per_discriminator_value
|
|
Prism::RateLimiter.throttle('logins/ip', limit: 1, period: 60) { |req| req.env['REMOTE_ADDR'] }
|
|
|
|
assert_equal 200, request(ip: '1.1.1.1').first
|
|
assert_equal 200, request(ip: '2.2.2.2').first
|
|
assert_equal 429, request(ip: '1.1.1.1').first
|
|
end
|
|
|
|
def test_discriminator_returning_nil_skips_the_rule
|
|
Prism::RateLimiter.throttle('logins/ip', limit: 1, period: 60) { |_req| nil }
|
|
|
|
5.times { assert_equal 200, request.first }
|
|
end
|
|
|
|
def test_blocklisted_request_is_rejected_with_403_before_reaching_app
|
|
Prism::RateLimiter.blocklist('scanners') { |req| req.path == '/etc/passwd' }
|
|
|
|
status, = request(path: '/etc/passwd')
|
|
assert_equal 403, status
|
|
assert_equal 0, @downstream_calls
|
|
end
|
|
|
|
def test_safelist_bypasses_an_otherwise_matching_blocklist
|
|
Prism::RateLimiter.blocklist('scanners') { |_req| true }
|
|
Prism::RateLimiter.safelist('trusted') { |req| req.env['REMOTE_ADDR'] == '9.9.9.9' }
|
|
|
|
assert_equal 200, request(ip: '9.9.9.9').first
|
|
assert_equal 403, request(ip: '1.1.1.1').first
|
|
end
|
|
|
|
def test_track_rule_observes_but_never_blocks
|
|
tracked = []
|
|
Prism::RateLimiter.track('all-logins') { |req| req.env['REMOTE_ADDR'] }
|
|
Prism::RateLimiter.on_notify { |type, name, _req, payload| tracked << [type, name, payload] }
|
|
|
|
assert_equal 200, request(ip: '3.3.3.3').first
|
|
assert_equal [[:track, 'all-logins', '3.3.3.3']], tracked
|
|
end
|
|
|
|
def test_fail2ban_style_blocklist_bans_after_repeated_offenses
|
|
Prism::RateLimiter.blocklist('fail2ban pentesters') do |req|
|
|
Prism::RateLimiter.fail2ban_filter(req.env['REMOTE_ADDR'], maxretry: 2, findtime: 60, bantime: 300) do
|
|
req.path.include?('/etc/passwd')
|
|
end
|
|
end
|
|
|
|
assert_equal 200, request(ip: '5.5.5.5', path: '/etc/passwd').first
|
|
assert_equal 403, request(ip: '5.5.5.5', path: '/etc/passwd').first
|
|
assert_equal 403, request(ip: '5.5.5.5', path: '/harmless').first, 'stays banned without a new offense'
|
|
assert_equal 200, request(ip: '6.6.6.6', path: '/etc/passwd').first, 'other IPs are unaffected'
|
|
end
|
|
end
|