diff --git a/README.md b/README.md index 545003cc..d41a38ba 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ See the [Backing & Hacking blog post](https://www.kickstarter.com/backing-and-ha - [Cache store configuration](#cache-store-configuration) - [Customizing responses](#customizing-responses) - [RateLimit headers for well-behaved clients](#ratelimit-headers-for-well-behaved-clients) +- [Blocking Based on Response](#blocking-based-on-response) - [Logging & Instrumentation](#logging--instrumentation) - [Testing](#testing) - [How it works](#how-it-works) @@ -373,6 +374,14 @@ For responses that did not exceed a throttle limit, Rack::Attack annotates the e request.env['rack.attack.throttle_data'][name] # => { discriminator: d, count: n, period: p, limit: l, epoch_time: t } ``` +## Blocking Based on Response + +Sometimes you want to block requests based on the response rather than just the request properties. For example, you might want to block IPs that receive multiple 404 responses (potential pentesters scanning for vulnerabilities) or multiple 401 responses (brute force login attempts). + +Since `Rack::Attack` runs before your application processes the request, it cannot directly access response status codes. However, you can achieve this using a custom middleware that runs *after* your application, combined with `Fail2Ban` filters. + +For a complete working example with middleware implementation and setup instructions, see the [Block Based on Response](docs/advanced_configuration.md#block-based-on-response) section in the advanced configuration guide. + ## Logging & Instrumentation Rack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available. diff --git a/docs/advanced_configuration.md b/docs/advanced_configuration.md index 6d8737ea..9cc9e635 100644 --- a/docs/advanced_configuration.md +++ b/docs/advanced_configuration.md @@ -92,6 +92,118 @@ Rack::Attack.blocklist('basic auth crackers') do |req| end ``` +### Block Based on Response + +Sometimes you want to block based on the HTTP response, rather than just the request properties. Common use cases include: + +- **Blocking pentesters**: IPs that receive multiple 404 responses are likely scanning for vulnerabilities +- **Blocking brute force attacks**: IPs that receive multiple 401 responses are likely attempting to guess credentials +- **Blocking scrapers**: IPs that trigger many error responses may be poorly configured bots + +Since `Rack::Attack` runs as middleware *before* your application processes requests, it cannot directly access response properties. The solution is to create a custom middleware that runs *after* your application and uses `Fail2Ban` to track response properties. + +#### Example implementation + +First, create a middleware that tracks response status codes. This middleware should be placed in your application (e.g., `app/middleware/response_tracker_middleware.rb` in Rails): + +```ruby +# app/middleware/response_tracker_middleware.rb +class ResponseTrackerMiddleware + # Define filters that will be shared between this middleware and Rack::Attack + FILTERS = [ + ->(request, condition = nil) do + # Track IPs that receive 404 responses + # After 50 404s in 10 seconds, ban the IP for 60 seconds + Rack::Attack::Fail2Ban.filter("pentesters-#{request.ip}", maxretry: 50, findtime: 10, bantime: 60) { condition } + end + ] + + def initialize(app) + @app = app + end + + def call(env) + # Let the application process the request first + status, headers, body = @app.call(env) + + # Check the response status and update Fail2Ban counters + request = Rack::Attack::Request.new(env) + FILTERS.each do |filter| + # Increment the counter if status is 404 + filter.call(request, status == 404) + end + + [status, headers, body] + end +end +``` + +Next, configure `Rack::Attack` to use the same filter for blocking. Add this to your Rack::Attack initializer (e.g., `config/initializers/rack_attack.rb`): + +```ruby +# config/initializers/rack_attack.rb +# Use the same filters defined in ResponseTrackerMiddleware +# This checks if an IP should be blocked, but does NOT increment the counter +ResponseTrackerMiddleware::FILTERS.each do |filter| + Rack::Attack.blocklist('pentesters by 404') do |request| + filter.call(request) + end +end +``` + +Finally, add the middleware to your application stack. + +```ruby +# config/application.rb (for Rails) +config.middleware.use ResponseTrackerMiddleware +``` + +Or for a Rack application: + +```ruby +# config.ru +use Rack::Attack +use ResponseTrackerMiddleware +run YourApp +``` + +#### How It Works + +1. A request comes in and passes through `Rack::Attack` first +2. `Rack::Attack` checks the blocklist, which calls the filter *without* incrementing the counter +3. If the IP is already banned (from previous 404s), the request is blocked with a 403 response +4. If not blocked, the request continues to your application +5. Your application processes the request and returns a response (e.g., 404) +6. `ResponseTrackerMiddleware` runs after your app and checks the status code +7. If status is 404, it increments the Fail2Ban counter for that IP +8. Once the IP exceeds `maxretry` 404s within `findtime`, subsequent requests are blocked + +#### Customization + +You can adapt this pattern for other use cases: + +```ruby +# Block IPs with multiple 401 responses (failed login attempts) +->(request, condition = nil) do + Rack::Attack::Fail2Ban.filter("login-failures-#{request.ip}", maxretry: 5, findtime: 1.minute, bantime: 10.minutes) { condition } +end + +# Then in the middleware: +filter.call(request, status == 401 && request.path == '/login') +``` + +```ruby +# Block IPs with any 4xx error +->(request, condition = nil) do + Rack::Attack::Fail2Ban.filter("client-errors-#{request.ip}", maxretry: 10, findtime: 5.minutes, bantime: 1.hour) { condition } +end + +# Then in the middleware: +filter.call(request, status >= 400 && status < 500) +``` + +**Note**: The same filter instance must be used in both the middleware (to increment counters) and the Rack::Attack blocklist (to check if IP should be blocked). + ### Match Actions in Rails Instead of matching the URL with complex regex, it can be much easier to match specific controller actions: