Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Security: Add request rate limiting (#2401)
* Security: Add request rate limiting

* Include password reset and API endpoints to rate limit

* Rate Limiting: Use env variable to denote Cloudfare Reverse Proxy usage

* Assume Cloudfare disabled by default

Co-authored-by: Aboobacker MK <aboobackervyd@gmail.com>
  • Loading branch information
gr455 and tachyons committed Aug 25, 2021
1 parent 13d4cc6 commit 879d145
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Gemfile
Expand Up @@ -166,3 +166,6 @@ gem "newrelic_rpm", "~> 6.13"
gem "oj", "~> 3.12"

gem "hairtrigger", "~> 0.2.24"

# Used for rate limiting
gem "rack-attack"
5 changes: 4 additions & 1 deletion Gemfile.lock
Expand Up @@ -411,6 +411,8 @@ GEM
activesupport (>= 3.0.0)
racc (1.5.2)
rack (2.2.3)
rack-attack (6.5.0)
rack (>= 1.0, < 3)
rack-pjax (1.1.0)
nokogiri (~> 1.5)
rack (>= 1.1)
Expand Down Expand Up @@ -725,6 +727,7 @@ DEPENDENCIES
pry-rails
puma (~> 5.3)
pundit
rack-attack
rails (~> 6.0)
rails-erd
rails-i18n (~> 6.0.0)
Expand Down Expand Up @@ -761,4 +764,4 @@ DEPENDENCIES
will_paginate-bootstrap

BUNDLED WITH
2.2.25
2.2.26
3 changes: 3 additions & 0 deletions config/application.rb
Expand Up @@ -21,6 +21,9 @@ class Application < Rails::Application
config.i18n.available_locales = [:en, :hi]
config.i18n.default_locale = :en

# configuring middleware
config.middleware.use Rack::Attack

# Site config
config.site_url = "https://circuitverse.org/"
config.site_name = "CircuitVerse"
Expand Down
3 changes: 3 additions & 0 deletions config/environments/test.rb
Expand Up @@ -38,6 +38,9 @@
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr

# Disable Rack::Attack in test env to not throttle tests
config.middleware.delete Rack::Attack

# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
config.vapid_public_key = "BP0eSFqHWrs8xtF96UegaSl5rZJDbPkRen_9oQPZfq9q6iFmbwuELSKqm89qydRcG_F5xSsavxvbGyh_ci9_SQM="
Expand Down
85 changes: 85 additions & 0 deletions config/initializers/rack_attack.rb
@@ -0,0 +1,85 @@
# frozen_string_literal: true

class Rack::Attack
class Request < ::Rack::Request
# Take remote IP from Cloudfare's headers instead of rev proxy IP
def remote_ip
if ENV["CF_PROXY_ENABLED"]
# Cloudflare stores remote IP in CF_CONNECTING_IP header
@remote_ip ||= (env["HTTP_CF_CONNECTING_IP"] ||
env["action_dispatch.remote_ip"] ||
ip).to_s
else
@remote_ip ||= ip
end
end

# Hack to get JSON request params
# Reads from the IO stream and resets the stream
def json_params
unless @json_params
@json_params = JSON.parse env["rack.input"].read
env["rack.input"].rewind
end
@json_params
end
end

# Disable if DISABLE_RACK_ATTACK and if env is not production
# Disabled in test env in environments/test.rb
Rack::Attack.enabled = !ENV["DISABLE_RACK_ATTACK"] unless Rails.env.production?

### Throttle logins ###

# Throttle by IP
throttle("throttle logins by ip", limit: 5, period: 20.seconds) do |req|
req.remote_ip if req.path == "/users/sign_in" && req.post?
end

# Throttle by email
throttle("throttle logins by email", limit: 5, period: 20.seconds) do |req|
req.params["user"]["email"].to_s.downcase if req.path == "/users/sign_in" && req.post?
end

### Throttle password resets ###

# Throttle by IP
throttle("throttle password resets by ip", limit: 5, period: 20.seconds) do |req|
req.remote_ip if req.path == "/users/password" && req.post?
end

# Throttle by email
throttle("throttle password resets by email", limit: 5, period: 20.seconds) do |req|
req.params["user"]["email"].to_s.downcase if req.path == "/users/password" && req.post?
end

### Throttle logins on API ###

# Throttle by IP
throttle("throttle api logins by ip", limit: 5, period: 20.seconds) do |req|
req.remote_ip if req.path == "/api/v1/auth/login" && req.post?
end

# Throttle by email
throttle("throttle api logins by email", limit: 5, period: 20.seconds) do |req|
req.json_params["email"].to_s.downcase if req.path == "/api/v1/auth/login" && req.post?
end

### Throttle password resets on API ###

# Throttle by IP
throttle("throttle api password resets by ip", limit: 5, period: 20.seconds) do |req|
req.remote_ip if req.path == "/api/v1/password/forgot" && req.post?
end

# Throttle by email
throttle("throttle api password resets by email", limit: 5, period: 20.seconds) do |req|
req.json_params["email"].to_s.downcase if req.path == "/api/v1/password/forgot" && req.post?
end

self.throttled_response = lambda do |_env|
[429, # status
{}, # headers
["Too many requests, please try again later"]] # body
end
end

0 comments on commit 879d145

Please sign in to comment.