{"id":13428151,"url":"https://github.com/rack/rack-attack","last_synced_at":"2025-09-09T21:18:47.983Z","repository":{"id":4109614,"uuid":"5219382","full_name":"rack/rack-attack","owner":"rack","description":"Rack middleware for blocking \u0026 throttling","archived":false,"fork":false,"pushed_at":"2025-02-12T22:37:28.000Z","size":966,"stargazers_count":5613,"open_issues_count":13,"forks_count":340,"subscribers_count":162,"default_branch":"main","last_synced_at":"2025-05-05T20:45:54.593Z","etag":null,"topics":["rack","rack-attack","rack-middleware","ruby"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rack.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2012-07-29T02:19:18.000Z","updated_at":"2025-05-03T20:23:33.000Z","dependencies_parsed_at":"2023-07-05T18:48:07.568Z","dependency_job_id":"fe4a730c-7018-4f69-ab18-18d92458c6ff","html_url":"https://github.com/rack/rack-attack","commit_stats":{"total_commits":626,"total_committers":98,"mean_commits":6.387755102040816,"dds":0.6022364217252396,"last_synced_commit":"0d40ea6538c95fb3c6f70862356d2cb54c02b02e"},"previous_names":["kickstarter/rack-attack"],"tags_count":49,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rack%2Frack-attack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rack%2Frack-attack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rack%2Frack-attack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rack%2Frack-attack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rack","download_url":"https://codeload.github.com/rack/rack-attack/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252659279,"owners_count":21784170,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rack","rack-attack","rack-middleware","ruby"],"created_at":"2024-07-31T01:00:47.450Z","updated_at":"2025-09-09T21:18:47.966Z","avatar_url":"https://github.com/rack.png","language":"Ruby","readme":":warning:  You are viewing the development's branch version of README which might contain documentation for unreleased features.\nFor the README consistent with the latest released version see https://github.com/rack/rack-attack/blob/6-stable/README.md.\n\n# Rack::Attack\n\n*Rack middleware for blocking \u0026 throttling abusive requests*\n\nProtect your Rails and Rack apps from bad clients. Rack::Attack lets you easily decide when to *allow*, *block* and *throttle* based on properties of the request.\n\nSee the [Backing \u0026 Hacking blog post](https://www.kickstarter.com/backing-and-hacking/rack-attack-protection-from-abusive-clients) introducing Rack::Attack.\n\n[![Gem Version](https://badge.fury.io/rb/rack-attack.svg)](https://badge.fury.io/rb/rack-attack)\n[![build](https://github.com/rack/rack-attack/actions/workflows/build.yml/badge.svg)](https://github.com/rack/rack-attack/actions/workflows/build.yml)\n[![Join the chat at https://gitter.im/rack-attack/rack-attack](https://badges.gitter.im/rack-attack/rack-attack.svg)](https://gitter.im/rack-attack/rack-attack)\n\n## Table of contents\n\n- [Getting started](#getting-started)\n  - [Installing](#installing)\n  - [Plugging into the application](#plugging-into-the-application)\n- [Usage](#usage)\n  - [Safelisting](#safelisting)\n    - [`safelist_ip(ip_address_string)`](#safelist_ipip_address_string)\n    - [`safelist_ip(ip_subnet_string)`](#safelist_ipip_subnet_string)\n    - [`safelist(name, \u0026block)`](#safelistname-block)\n  - [Blocking](#blocking)\n    - [`blocklist_ip(ip_address_string)`](#blocklist_ipip_address_string)\n    - [`blocklist_ip(ip_subnet_string)`](#blocklist_ipip_subnet_string)\n    - [`blocklist(name, \u0026block)`](#blocklistname-block)\n    - [Fail2Ban](#fail2ban)\n    - [Allow2Ban](#allow2ban)\n  - [Throttling](#throttling)\n    - [`throttle(name, options, \u0026block)`](#throttlename-options-block)\n  - [Tracks](#tracks)\n  - [Cache store configuration](#cache-store-configuration)\n- [Customizing responses](#customizing-responses)\n  - [RateLimit headers for well-behaved clients](#ratelimit-headers-for-well-behaved-clients)\n- [Logging \u0026 Instrumentation](#logging--instrumentation)\n- [Testing](#testing)\n- [How it works](#how-it-works)\n  - [About Tracks](#about-tracks)\n- [Performance](#performance)\n- [Motivation](#motivation)\n- [Contributing](#contributing)\n- [Code of Conduct](#code-of-conduct)\n- [Development setup](#development-setup)\n- [License](#license)\n\n## Getting started\n\n### Installing\n\nAdd this line to your application's Gemfile:\n\n```ruby\n# In your Gemfile\n\ngem 'rack-attack'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install rack-attack\n\n### Plugging into the application\n\nThen tell your ruby web application to use rack-attack as a middleware.\n\na) For __rails__ applications it is used by default.\n\nYou can disable it permanently (like for specific environment) or temporarily (can be useful for specific test cases) by writing:\n\n```ruby\nRack::Attack.enabled = false\n```\n\nb) For __rack__ applications:\n\n```ruby\n# In config.ru\n\nrequire \"rack/attack\"\nuse Rack::Attack\n```\n\n__IMPORTANT__: By default, rack-attack won't perform any blocking or throttling, until you specifically tell it what to protect against by configuring some rules.\n\n## Usage\n\n*Tip:* If you just want to get going asap, then you can take our [example configuration](docs/example_configuration.md)\nand tailor it to your needs, or check out the [advanced configuration](docs/advanced_configuration.md) examples.\n\nDefine rules by calling `Rack::Attack` public methods, in any file that runs when your application is being initialized. For rails applications this means creating a new file named `config/initializers/rack_attack.rb` and writing your rules there.\n\n### Safelisting\n\nSafelists have the most precedence, so any request matching a safelist would be allowed despite matching any number of blocklists or throttles.\n\n#### `safelist_ip(ip_address_string)`\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails app)\n\nRack::Attack.safelist_ip(\"5.6.7.8\")\n```\n\n#### `safelist_ip(ip_subnet_string)`\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails app)\n\nRack::Attack.safelist_ip(\"5.6.7.0/24\")\n```\n\n#### `safelist(name, \u0026block)`\n\nName your custom safelist and make your ruby-block argument return a truthy value if you want the request to be allowed, and falsy otherwise.\n\nThe request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails apps)\n\n# Provided that trusted users use an HTTP request header named APIKey\nRack::Attack.safelist(\"mark any authenticated access safe\") do |request|\n  # Requests are allowed if the return value is truthy\n  request.env[\"HTTP_APIKEY\"] == \"secret-string\"\nend\n\n# Always allow requests from localhost\n# (blocklist \u0026 throttles are skipped)\nRack::Attack.safelist('allow from localhost') do |req|\n  # Requests are allowed if the return value is truthy\n  '127.0.0.1' == req.ip || '::1' == req.ip\nend\n```\n\n### Blocking\n\n#### `blocklist_ip(ip_address_string)`\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails apps)\n\nRack::Attack.blocklist_ip(\"1.2.3.4\")\n```\n\n#### `blocklist_ip(ip_subnet_string)`\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails apps)\n\nRack::Attack.blocklist_ip(\"1.2.0.0/16\")\n```\n\n#### `blocklist(name, \u0026block)`\n\nName your custom blocklist and make your ruby-block argument return a truthy value if you want the request to be blocked, and falsy otherwise.\n\nThe request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails apps)\n\nRack::Attack.blocklist(\"block all access to admin\") do |request|\n  # Requests are blocked if the return value is truthy\n  request.path.start_with?(\"/admin\")\nend\n\nRack::Attack.blocklist('block bad UA logins') do |req|\n  req.path == '/login' \u0026\u0026 req.post? \u0026\u0026 req.user_agent == 'BadUA'\nend\n```\n\n#### Fail2Ban\n\n`Fail2Ban.filter` can be used within a blocklist to block all requests from misbehaving clients.\nThis pattern is inspired by [fail2ban](https://www.fail2ban.org/wiki/index.php/Main_Page).\nSee the [fail2ban documentation](https://www.fail2ban.org/wiki/index.php/MANUAL_0_8#Jail_Options) for more details on\nhow the parameters work.  For multiple filters, be sure to put each filter in a separate blocklist and use a unique discriminator for each fail2ban filter.\n\nFail2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).\n\n```ruby\n# Block suspicious requests for '/etc/password' or wordpress specific paths.\n# After 3 blocked requests in 10 minutes, block all requests from that IP for 5 minutes.\nRack::Attack.blocklist('fail2ban pentesters') do |req|\n  # `filter` returns truthy value if request fails, or if it's from a previously banned IP\n  # so the request is blocked\n  Rack::Attack::Fail2Ban.filter(\"pentesters-#{req.ip}\", maxretry: 3, findtime: 10.minutes, bantime: 5.minutes) do\n    # The count for the IP is incremented if the return value is truthy\n    CGI.unescape(req.query_string) =~ %r{/etc/passwd} ||\n    req.path.include?('/etc/passwd') ||\n    req.path.include?('wp-admin') ||\n    req.path.include?('wp-login')\n\n  end\nend\n```\n\nNote that `Fail2Ban` filters are not automatically scoped to the blocklist, so when using multiple filters in an application the scoping must be added to the discriminator e.g. `\"pentest:#{req.ip}\"`.\n\n#### Allow2Ban\n\n`Allow2Ban.filter` works the same way as the `Fail2Ban.filter` except that it *allows* requests from misbehaving\nclients until such time as they reach maxretry at which they are cut off as per normal.\n\nAllow2ban state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).\n\n```ruby\n# Lockout IP addresses that are hammering your login page.\n# After 20 requests in 1 minute, block all requests from that IP for 1 hour.\nRack::Attack.blocklist('allow2ban login scrapers') do |req|\n  # `filter` returns false value if request is to your login page (but still\n  # increments the count) so request below the limit are not blocked until\n  # they hit the limit.  At that point, filter will return true and block.\n  Rack::Attack::Allow2Ban.filter(req.ip, maxretry: 20, findtime: 1.minute, bantime: 1.hour) do\n    # The count for the IP is incremented if the return value is truthy.\n    req.path == '/login' and req.post?\n  end\nend\n```\n\n### Throttling\n\nThrottle state is stored in a [configurable cache](#cache-store-configuration) (which defaults to `Rails.cache` if present).\n\n#### `throttle(name, options, \u0026block)`\n\nName your custom throttle, provide `limit` and `period` as options, and make your ruby-block argument return the __discriminator__. This discriminator is how you tell rack-attack whether you're limiting per IP address, per user email or any other.\n\nThe request object is a [Rack::Request](http://www.rubydoc.info/gems/rack/Rack/Request).\n\nE.g.\n\n```ruby\n# config/initializers/rack_attack.rb (for rails apps)\n\nRack::Attack.throttle(\"requests by ip\", limit: 5, period: 2) do |request|\n  request.ip\nend\n\n# Throttle login attempts for a given email parameter to 6 reqs/minute\n# Return the *normalized* email as a discriminator on POST /login requests\nRack::Attack.throttle('limit logins per email', limit: 6, period: 60) do |req|\n  if req.path == '/login' \u0026\u0026 req.post?\n    # Normalize the email, using the same logic as your authentication process, to\n    # protect against rate limit bypasses.\n    req.params['email'].to_s.downcase.gsub(/\\s+/, \"\")\n  end\nend\n\n# You can also set a limit and period using a proc. For instance, after\n# Rack::Auth::Basic has authenticated the user:\nlimit_proc = proc { |req| req.env[\"REMOTE_USER\"] == \"admin\" ? 100 : 1 }\nperiod_proc = proc { |req| req.env[\"REMOTE_USER\"] == \"admin\" ? 1 : 60 }\n\nRack::Attack.throttle('request per ip', limit: limit_proc, period: period_proc) do |request|\n  request.ip\nend\n```\n\n### Tracks\n\n```ruby\n# Track requests from a special user agent.\nRack::Attack.track(\"special_agent\") do |req|\n  req.user_agent == \"SpecialAgent\"\nend\n\n# Supports optional limit and period, triggers the notification only when the limit is reached.\nRack::Attack.track(\"special_agent\", limit: 6, period: 60) do |req|\n  req.user_agent == \"SpecialAgent\"\nend\n\n# Track it using ActiveSupport::Notification\nActiveSupport::Notifications.subscribe(\"track.rack_attack\") do |name, start, finish, instrumenter_id, payload|\n  req = payload[:request]\n  if req.env['rack.attack.matched'] == \"special_agent\"\n    Rails.logger.info \"special_agent: #{req.path}\"\n    STATSD.increment(\"special_agent\")\n  end\nend\n```\n\n### Cache store configuration\n\nThrottle, track, allow2ban and fail2ban state is stored in a configurable cache (which defaults to `Rails.cache` if present), presumably backed by memcached or redis ([at least gem v3.0.0](https://rubygems.org/gems/redis)).\n\n```ruby\n# This is the default\nRack::Attack.cache.store = Rails.cache \n# It is recommended to use a separate database for throttling/allow2ban/fail2ban.\nRack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: \"...\") \n```\n\nMost applications should use a new, separate database used only for `rack-attack`. During an actual attack or periods of heavy load, this database will come under heavy load. Keeping it on a separate database instance will give you additional resilience and make sure that other functions (like caching for your application) don't go down.\n\nNote that `Rack::Attack.cache` is only used for throttling, allow2ban and fail2ban filtering; not blocklisting and safelisting. Your cache store must implement `increment` and `write` like [ActiveSupport::Cache::Store](http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html). This means that other cache stores which inherit from ActiveSupport::Cache::Store are also compatible. In-memory stores which are not backed by an external database, such as `ActiveSupport::Cache::MemoryStore.new`, will be mostly ineffective because each Ruby process in your deployment will have it's own state, effectively multiplying the number of requests each client can make by the number of Ruby processes you have deployed.\n\n## Customizing responses\n\nCustomize the response of blocklisted and throttled requests using an object that adheres to the [Rack app interface](http://www.rubydoc.info/github/rack/rack/file/SPEC.rdoc).\n\n```ruby\nRack::Attack.blocklisted_responder = lambda do |request|\n  # Using 503 because it may make attacker think that they have successfully\n  # DOSed the site. Rack::Attack returns 403 for blocklists by default\n  [ 503, {}, ['Blocked']]\nend\n\nRack::Attack.throttled_responder = lambda do |request|\n  # NB: you have access to the name and other data about the matched throttle\n  #  request.env['rack.attack.matched'],\n  #  request.env['rack.attack.match_type'],\n  #  request.env['rack.attack.match_data'],\n  #  request.env['rack.attack.match_discriminator']\n\n  # Using 503 because it may make attacker think that they have successfully\n  # DOSed the site. Rack::Attack returns 429 for throttling by default\n  [ 503, {}, [\"Server Error\\n\"]]\nend\n```\n\n### RateLimit headers for well-behaved clients\n\nWhile Rack::Attack's primary focus is minimizing harm from abusive clients, it\ncan also be used to return rate limit data that's helpful for well-behaved clients.\n\nIf you want to return to user how many seconds to wait until they can start sending requests again, this can be done through enabling `Retry-After` header:\n```ruby\nRack::Attack.throttled_response_retry_after_header = true\n```\n\nHere's an example response that includes conventional `RateLimit-*` headers:\n\n```ruby\nRack::Attack.throttled_responder = lambda do |request|\n  match_data = request.env['rack.attack.match_data']\n  now = match_data[:epoch_time]\n\n  headers = {\n    'RateLimit-Limit' =\u003e match_data[:limit].to_s,\n    'RateLimit-Remaining' =\u003e '0',\n    'RateLimit-Reset' =\u003e (now + (match_data[:period] - now % match_data[:period])).to_s\n  }\n\n  [ 429, headers, [\"Throttled\\n\"]]\nend\n```\n\n\nFor responses that did not exceed a throttle limit, Rack::Attack annotates the env with match data:\n\n```ruby\nrequest.env['rack.attack.throttle_data'][name] # =\u003e { discriminator: d, count: n, period: p, limit: l, epoch_time: t }\n```\n\n## Logging \u0026 Instrumentation\n\nRack::Attack uses the [ActiveSupport::Notifications](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html) API if available.\n\nYou can subscribe to `rack_attack` events and log it, graph it, etc.\n\nTo get notified about specific type of events, subscribe to the event name followed by the `rack_attack` namespace.\nE.g. for throttles use:\n\n```ruby\nActiveSupport::Notifications.subscribe(\"throttle.rack_attack\") do |name, start, finish, instrumenter_id, payload|\n  # request object available in payload[:request]\n\n  # Your code here\nend\n```\n\nIf you want to subscribe to every `rack_attack` event, use:\n\n```ruby\nActiveSupport::Notifications.subscribe(/rack_attack/) do |name, start, finish, instrumenter_id, payload|\n  # request object available in payload[:request]\n\n  # Your code here\nend\n```\n\n## Testing\n\nA note on developing and testing apps using Rack::Attack - if you are using throttling in particular, you will\nneed to enable the cache in your development environment. See [Caching with Rails](http://guides.rubyonrails.org/caching_with_rails.html)\nfor more on how to do this.\n\n### Disabling\n\n`Rack::Attack.enabled = false` can be used to either completely disable Rack::Attack in your tests, or to disable/enable for specific test cases only.\n\n### Test case isolation\n\n`Rack::Attack.reset!` can be used in your test suite to clear any Rack::Attack state between different test cases. If you're testing blocklist and safelist configurations, consider using `Rack::Attack.clear_configuration` to unset the values for those lists between test cases.\n\n## How it works\n\nThe Rack::Attack middleware compares each request against *safelists*, *blocklists*, *throttles*, and *tracks* that you define. There are none by default.\n\n * If the request matches any **safelist**, it is allowed.\n * Otherwise, if the request matches any **blocklist**, it is blocked.\n * Otherwise, if the request matches any **throttle**, a counter is incremented in the Rack::Attack.cache. If any throttle's limit is exceeded, the request is blocked.\n * Otherwise, all **tracks** are checked, and the request is allowed.\n\nThe algorithm is actually more concise in code: See [Rack::Attack.call](lib/rack/attack.rb):\n\n```ruby\ndef call(env)\n  req = Rack::Attack::Request.new(env)\n\n  if safelisted?(req)\n    @app.call(env)\n  elsif blocklisted?(req)\n    self.class.blocklisted_responder.call(req)\n  elsif throttled?(req)\n    self.class.throttled_responder.call(req)\n  else\n    tracked?(req)\n    @app.call(env)\n  end\nend\n```\n\nNote: `Rack::Attack::Request` is just a subclass of `Rack::Request` so that you\ncan cleanly monkey patch helper methods onto the\n[request object](lib/rack/attack/request.rb).\n\n### About Tracks\n\n`Rack::Attack.track` doesn't affect request processing. Tracks are an easy way to log and measure requests matching arbitrary attributes.\n\n## Performance\n\nThe overhead of running Rack::Attack is typically negligible (a few milliseconds per request),\nbut it depends on how many checks you've configured, and how long they take.\nThrottles usually require a network roundtrip to your cache server(s),\nso try to keep the number of throttle checks per request low.\n\nIf a request is blocklisted or throttled, the response is a very simple Rack response.\nA single typical ruby web server thread can block several hundred requests per second.\n\nRack::Attack complements tools like `iptables` and nginx's [limit_conn_zone module](https://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone).\n\n## Motivation\n\nAbusive clients range from malicious login crackers to naively-written scrapers.\nThey hinder the security, performance, \u0026 availability of web applications.\n\nIt is impractical if not impossible to block abusive clients completely.\n\nRack::Attack aims to let developers quickly mitigate abusive requests and rely\nless on short-term, one-off hacks to block a particular attack.\n\n## Contributing\n\nCheck out the [Contributing guide](CONTRIBUTING.md).\n\n## Code of Conduct\n\nThis project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md).\n\n## Development setup\n\nCheck out the [Development guide](docs/development.md).\n\n## License\n\nCopyright Kickstarter, PBC.\n\nReleased under an [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":["Production","Ruby","Gems"],"sub_categories":["Security"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frack%2Frack-attack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frack%2Frack-attack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frack%2Frack-attack/lists"}