From dccce4ee3db688ba044f6c77db90eb65f94045b2 Mon Sep 17 00:00:00 2001 From: Gonzalo Rodriguez Date: Mon, 26 Mar 2018 17:33:58 -0300 Subject: [PATCH 1/5] Provide shorthand to blocklist an IP --- CHANGELOG.md | 1 + lib/rack/attack.rb | 17 ++++++++++--- lib/rack/attack/check.rb | 1 + spec/acceptance/blocking_ip_spec.rb | 38 +++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 spec/acceptance/blocking_ip_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e114486..95bd8ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ### Added +- Shorthand for blocking IP addresses `Rack::Attack.blocklist_ip("1.2.3.4")` - Throw helpful error message when using `allow2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) - Throw helpful error message when using `fail2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index dd151fd..f630167 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -37,6 +37,12 @@ class Rack::Attack self.blocklists[name] = Blocklist.new(name, block) end + def blocklist_ip(ip) + @ip_blocklists ||= [] + ip_blocklist_proc = lambda { |request| request.ip == ip } + @ip_blocklists << Blocklist.new(nil, ip_blocklist_proc) + end + def blacklist(name, &block) warn "[DEPRECATION] 'Rack::Attack.blacklist' is deprecated. Please use 'blocklist' instead." blocklist(name, &block) @@ -77,9 +83,8 @@ class Rack::Attack end def blocklisted?(req) - blocklists.any? do |name, blocklist| - blocklist[req] - end + ip_blocklists.any? { |blocklist| blocklist.match?(req) } || + blocklists.any? { |_name, blocklist| blocklist.match?(req) } end def blacklisted?(req) @@ -109,6 +114,7 @@ class Rack::Attack def clear! @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {} + @ip_blocklists = [] end def blacklisted_response=(res) @@ -121,6 +127,11 @@ class Rack::Attack blocklisted_response end + private + + def ip_blocklists + @ip_blocklists ||= [] + end end # Set defaults diff --git a/lib/rack/attack/check.rb b/lib/rack/attack/check.rb index 21451de..8ae9ba1 100644 --- a/lib/rack/attack/check.rb +++ b/lib/rack/attack/check.rb @@ -17,6 +17,7 @@ module Rack } end + alias_method :match?, :[] end end end diff --git a/spec/acceptance/blocking_ip_spec.rb b/spec/acceptance/blocking_ip_spec.rb new file mode 100644 index 0000000..72c599c --- /dev/null +++ b/spec/acceptance/blocking_ip_spec.rb @@ -0,0 +1,38 @@ +require_relative "../spec_helper" + +describe "Blocking an IP" do + before do + Rack::Attack.blocklist_ip("1.2.3.4") + end + + it "forbids request if IP matches" do + get "/", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert_equal 403, last_response.status + end + + it "succeeds if IP doesn't match" do + get "/", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + end + + it "notifies when the request is blocked" do + notified = false + notification_type = nil + + ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request| + notified = true + notification_type = request.env["rack.attack.match_type"] + end + + get "/", {}, "REMOTE_ADDR" => "5.6.7.8" + + refute notified + + get "/", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert notified + assert_equal :blocklist, notification_type + end +end From aec03047c56a66f6bfbd9dafa81c333a508d8630 Mon Sep 17 00:00:00 2001 From: Gonzalo Rodriguez Date: Mon, 26 Mar 2018 17:35:41 -0300 Subject: [PATCH 2/5] Provide shorthand to blocklist an entire IP subnet --- CHANGELOG.md | 1 + lib/rack/attack.rb | 2 +- spec/acceptance/blocking_subnet_spec.rb | 44 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 spec/acceptance/blocking_subnet_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 95bd8ce..146aee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Added - Shorthand for blocking IP addresses `Rack::Attack.blocklist_ip("1.2.3.4")` +- Shorthand for blocking IP subnets `Rack::Attack.blocklist_ip("1.2.0.0/16")` - Throw helpful error message when using `allow2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) - Throw helpful error message when using `fail2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index f630167..836f7a9 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -39,7 +39,7 @@ class Rack::Attack def blocklist_ip(ip) @ip_blocklists ||= [] - ip_blocklist_proc = lambda { |request| request.ip == ip } + ip_blocklist_proc = lambda { |request| IPAddr.new(ip).include?(IPAddr.new(request.ip)) } @ip_blocklists << Blocklist.new(nil, ip_blocklist_proc) end diff --git a/spec/acceptance/blocking_subnet_spec.rb b/spec/acceptance/blocking_subnet_spec.rb new file mode 100644 index 0000000..13b4f91 --- /dev/null +++ b/spec/acceptance/blocking_subnet_spec.rb @@ -0,0 +1,44 @@ +require_relative "../spec_helper" + +describe "Blocking an IP subnet" do + before do + Rack::Attack.blocklist_ip("1.2.3.4/31") + end + + it "forbids request if IP is inside the subnet" do + get "/", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert_equal 403, last_response.status + end + + it "forbids request for another IP in the subnet" do + get "/", {}, "REMOTE_ADDR" => "1.2.3.5" + + assert_equal 403, last_response.status + end + + it "succeeds if IP is outside the subnet" do + get "/", {}, "REMOTE_ADDR" => "1.2.3.6" + + assert_equal 200, last_response.status + end + + it "notifies when the request is blocked" do + notified = false + notification_type = nil + + ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request| + notified = true + notification_type = request.env["rack.attack.match_type"] + end + + get "/", {}, "REMOTE_ADDR" => "5.6.7.8" + + refute notified + + get "/", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert notified + assert_equal :blocklist, notification_type + end +end From 27aab72d490002a995140faf802252ab75730a8d Mon Sep 17 00:00:00 2001 From: Gonzalo Rodriguez Date: Mon, 26 Mar 2018 17:50:23 -0300 Subject: [PATCH 3/5] Provide shorthand to safelist an IP --- CHANGELOG.md | 3 +- lib/rack/attack.rb | 16 +++++++-- spec/acceptance/safelisting_ip_spec.rb | 49 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 spec/acceptance/safelisting_ip_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 146aee9..59223bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ All notable changes to this project will be documented in this file. ### Added -- Shorthand for blocking IP addresses `Rack::Attack.blocklist_ip("1.2.3.4")` +- Shorthand for blocking an IP address `Rack::Attack.blocklist_ip("1.2.3.4")` - Shorthand for blocking IP subnets `Rack::Attack.blocklist_ip("1.2.0.0/16")` +- Shorthand for safelisting an IP address `Rack::Attack.safelist_ip("5.6.7.8")` - Throw helpful error message when using `allow2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) - Throw helpful error message when using `fail2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index 836f7a9..4abca17 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -43,6 +43,12 @@ class Rack::Attack @ip_blocklists << Blocklist.new(nil, ip_blocklist_proc) end + def safelist_ip(ip) + @ip_safelists ||= [] + ip_safelist_proc = lambda { |request| ip == request.ip } + @ip_safelists << Safelist.new(nil, ip_safelist_proc) + end + def blacklist(name, &block) warn "[DEPRECATION] 'Rack::Attack.blacklist' is deprecated. Please use 'blocklist' instead." blocklist(name, &block) @@ -72,9 +78,8 @@ class Rack::Attack end def safelisted?(req) - safelists.any? do |name, safelist| - safelist[req] - end + ip_safelists.any? { |safelist| safelist.match?(req) } || + safelists.any? { |_name, safelist| safelist.match?(req) } end def whitelisted?(req) @@ -115,6 +120,7 @@ class Rack::Attack def clear! @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {} @ip_blocklists = [] + @ip_safelists = [] end def blacklisted_response=(res) @@ -132,6 +138,10 @@ class Rack::Attack def ip_blocklists @ip_blocklists ||= [] end + + def ip_safelists + @ip_safelists ||= [] + end end # Set defaults diff --git a/spec/acceptance/safelisting_ip_spec.rb b/spec/acceptance/safelisting_ip_spec.rb new file mode 100644 index 0000000..38faf72 --- /dev/null +++ b/spec/acceptance/safelisting_ip_spec.rb @@ -0,0 +1,49 @@ +require_relative "../spec_helper" + +describe "Safelist an IP" do + before do + Rack::Attack.blocklist("admin") do |request| + request.path == "/admin" + end + + Rack::Attack.safelist_ip("5.6.7.8") + end + + it "forbids request if blocklist condition is true and safelist is false" do + get "/admin", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert_equal 403, last_response.status + end + + it "succeeds if blocklist condition is false and safelist is false" do + get "/", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert_equal 200, last_response.status + end + + it "succeeds request if blocklist condition is false and safelist is true" do + get "/", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + end + + it "succeeds request if both blocklist and safelist conditions are true" do + get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + end + + it "notifies when the request is safe" do + notification_type = nil + + ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request| + notification_type = request.env["rack.attack.match_type"] + end + + get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + assert_equal :safelist, notification_type + end +end + From e907cc6b8342e72751b1cbcfac872799e3230321 Mon Sep 17 00:00:00 2001 From: Gonzalo Rodriguez Date: Mon, 26 Mar 2018 18:00:02 -0300 Subject: [PATCH 4/5] Provide shorthand to safelist an entire IP subnet --- CHANGELOG.md | 3 +- lib/rack/attack.rb | 2 +- spec/acceptance/safelisting_subnet_spec.rb | 48 ++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 spec/acceptance/safelisting_subnet_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 59223bc..afce862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ All notable changes to this project will be documented in this file. ### Added - Shorthand for blocking an IP address `Rack::Attack.blocklist_ip("1.2.3.4")` -- Shorthand for blocking IP subnets `Rack::Attack.blocklist_ip("1.2.0.0/16")` +- Shorthand for blocking an IP subnet `Rack::Attack.blocklist_ip("1.2.0.0/16")` - Shorthand for safelisting an IP address `Rack::Attack.safelist_ip("5.6.7.8")` +- Shorthand for safelisting an IP subnet `Rack::Attack.safelist_ip("5.6.0.0/16")` - Throw helpful error message when using `allow2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) - Throw helpful error message when using `fail2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index 4abca17..cf1379d 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -45,7 +45,7 @@ class Rack::Attack def safelist_ip(ip) @ip_safelists ||= [] - ip_safelist_proc = lambda { |request| ip == request.ip } + ip_safelist_proc = lambda { |request| IPAddr.new(ip).include?(IPAddr.new(request.ip)) } @ip_safelists << Safelist.new(nil, ip_safelist_proc) end diff --git a/spec/acceptance/safelisting_subnet_spec.rb b/spec/acceptance/safelisting_subnet_spec.rb new file mode 100644 index 0000000..920f265 --- /dev/null +++ b/spec/acceptance/safelisting_subnet_spec.rb @@ -0,0 +1,48 @@ +require_relative "../spec_helper" + +describe "Safelisting an IP subnet" do + before do + Rack::Attack.blocklist("admin") do |request| + request.path == "/admin" + end + + Rack::Attack.safelist_ip("5.6.0.0/16") + end + + it "forbids request if blocklist condition is true and safelist is false" do + get "/admin", {}, "REMOTE_ADDR" => "5.7.0.0" + + assert_equal 403, last_response.status + end + + it "succeeds if blocklist condition is false and safelist is false" do + get "/", {}, "REMOTE_ADDR" => "5.7.0.0" + + assert_equal 200, last_response.status + end + + it "succeeds request if blocklist condition is false and safelist is true" do + get "/", {}, "REMOTE_ADDR" => "5.6.0.0" + + assert_equal 200, last_response.status + end + + it "succeeds request if both blocklist and safelist conditions are true" do + get "/admin", {}, "REMOTE_ADDR" => "5.6.255.255" + + assert_equal 200, last_response.status + end + + it "notifies when the request is safe" do + notification_type = nil + + ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request| + notification_type = request.env["rack.attack.match_type"] + end + + get "/admin", {}, "REMOTE_ADDR" => "5.6.0.0" + + assert_equal 200, last_response.status + assert_equal :safelist, notification_type + end +end From 62aca946b50352f4900b2dddf1943f07cad4e9f0 Mon Sep 17 00:00:00 2001 From: Gonzalo Rodriguez Date: Mon, 26 Mar 2018 18:53:32 -0300 Subject: [PATCH 5/5] Require ipaddr so it works on ruby < 2.5 --- lib/rack/attack.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index cf1379d..35b2e35 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -2,6 +2,7 @@ require 'rack' require 'forwardable' require 'rack/attack/path_normalizer' require 'rack/attack/request' +require "ipaddr" class Rack::Attack class MisconfiguredStoreError < StandardError; end