Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reconfigure rack attack #24

Merged
merged 6 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ gem "sprockets", "~> 3.7"

gem "activejob-uniqueness", require: "active_job/uniqueness/sidekiq_patch"
gem "fog-aws"
gem "rack-attack", "~> 6.6"
gem "sys-filesystem"

group :development, :test do
Expand Down
5 changes: 3 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -675,8 +675,8 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.5.2)
rack (2.2.3)
rack-attack (6.5.0)
rack (2.2.6.4)
rack-attack (6.6.1)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
Expand Down Expand Up @@ -953,6 +953,7 @@ DEPENDENCIES
newrelic_rpm
passenger
puma (~> 5.3.1)
rack-attack (~> 6.6)
ruby-progressbar
sendgrid-ruby
sentry-rails
Expand Down
2 changes: 1 addition & 1 deletion config/i18n-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,5 @@ ignore_unused:
- decidim.anonymous_proposals.{new_session,register,shared.*}
- decidim.components.proposals.settings.global.anonymous_proposals_enabled
- decidim.components.anonymous_proposals.name

- rack_attack.too_many_requests.{message,time,time_unit,title}

150 changes: 144 additions & 6 deletions config/initializers/rack_attack.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,150 @@
# frozen_string_literal: true

if Rails.env.production? || Rails.env.test?
require "rack/attack"
# Enabled by default in production
# Can be deactivated with 'ENABLE_RACK_ATTACK=0'
return if Rails.application.secrets.dig(:decidim, :rack_attack, :enabled).zero?

class Rack::Attack
throttle("req/ip", limit: 100, period: 1.minute) do |req|
Rails.logger.warn("[Rack::Attack] [THROTTLE - req / ip] :: #{req.ip} :: #{req.path} :: #{req.GET}")
req.ip unless req.path.start_with?("/assets")
Rack::Attack.enabled = (Rails.application.secrets.dig(:decidim, :rack_attack, :enabled) == 1) || Rails.env.production?
Rack::Attack.throttled_response_retry_after_header = true

# By default use the memory store for inspecting requests
# Better to use MemCached or Redis in production mode
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new if !ENV["MEMCACHEDCLOUD_SERVERS"] || Rails.env.test?

# Remove the original throttle fron decidim-core
# see https://github.com/decidim/decidim/blob/release/0.26-stable/decidim-core/config/initializers/rack_attack.rb#L19
Rails.application.config.after_initialize do
Rack::Attack.throttles.delete("requests by ip")
end

Rack::Attack.throttled_responder = lambda do |request|
rack_logger = Logger.new(Rails.root.join("log/rack_attack.log"))
match_data = request.env["rack.attack.match_data"]
now = match_data[:epoch_time]
limit = now + (match_data[:period] - now % match_data[:period])

request_uuid = request.env["action_dispatch.request_id"]
params = {
"ip" => request.ip,
"path" => request.path,
"get" => request.GET,
"host" => request.host,
"referer" => request.referer
}

rack_logger.warn("[#{request_uuid}] #{params}")

[429, { "Content-Type" => "text/html" }, [html_template(limit - now, request.env["decidim.current_organization"]&.name)]]
end

Rack::Attack.throttle("req/ip",
limit: Rails.application.secrets.dig(:decidim, :rack_attack, :throttle, :max_requests),
period: Rails.application.secrets.dig(:decidim, :rack_attack, :throttle, :period)) do |req|
req.ip unless req.path.start_with?("/decidim-packs") || req.path.start_with?("/rails/active_storage") || req.path.start_with?("/admin/")
end

if Rails.application.secrets.dig(:decidim, :rack_attack, :fail2ban, :enabled) == 1
# Block suspicious requests made for pentesting
# After 1 forbidden request, block all requests from that IP for 1 hour.
Rack::Attack.blocklist("fail2ban pentesters") do |req|
# `filter` returns truthy value if request fails, or if it's from a previously banned IP
# so the request is blocked
Rack::Attack::Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 0, findtime: 10.minutes, bantime: 1.hour) do
# The count for the IP is incremented if the return value is truthy
req.url.include?("etc/passwd") ||
req.url.include?("wp-admin") ||
req.url.include?("wp-login") ||
req.url.include?("SELECT") ||
req.url.include?("CONCAT") ||
req.url.include?("UNION%20SELECT") ||
req.url.include?(".git")
end
end
end

def html_template(until_period, organization_name)
name = organization_name.presence || "our platform"

"
<!DOCTYPE html>
<html>
<head>
<title>Too many requests</title>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<style>
.rails-default-error-page {
background-color: #EFEFEF;
color: #2E2F30;
text-align: center;
font-family: arial, sans-serif;
margin: 0;
}
.rails-default-error-page div.dialog {
width: 95%;
max-width: 33em;
margin: 4em auto 0;
}
.rails-default-error-page div.dialog > div {
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #BBB;
border-top: #B00100 solid 4px;
border-top-left-radius: 9px;
border-top-right-radius: 9px;
background-color: white;
padding: 7px 12% 0;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
.rails-default-error-page h1 {
font-size: 100%;
color: #730E15;
line-height: 1.5em;
}
.rails-default-error-page div.dialog > p {
margin: 0 0 1em;
padding: 1em;
background-color: #F7F7F7;
border: 1px solid #CCC;
border-right-color: #999;
border-left-color: #999;
border-bottom-color: #999;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-top-color: #DADADA;
color: #666;
box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
}
</style>
</head>
<body class='rails-default-error-page'>
<div class='dialog'>
<div>
<b>#{I18n.t("rack_attack.too_many_requests.title", organization_name: name)}</b>
<br>
<h1>429 - Too many requests</h1>
<p>#{I18n.t("rack_attack.too_many_requests.message")}</p>
<b>#{I18n.t("rack_attack.too_many_requests.time")}</b>
<br>
<b class='counter'><span id='timer'>#{until_period}</span> #{I18n.t("rack_attack.too_many_requests.time_unit")}</b>
</div>
</div>
<script>
let timer = document.getElementById('timer')
let total = timer.textContent
const interval = setInterval(updateTimer, 1000)
function updateTimer() {
if (total <= 0) {
clearInterval(interval)
location.reload()
} else {
console.log(total)
timer.innerHTML = total
total -= 1
}
}
</script>
</body>
</html>
"
end
2 changes: 1 addition & 1 deletion config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ de:
newsletter: Gelegentlich einen Newsletter mit relevanten Informationen erhalten
newsletter_title: Kontakterlaubnis
nickname_help: Ihr Pseudonym in %{organization}. Kann nur Buchstaben, Zahlen, '-' und '_' enthalten.
password_help: "Mindestens %{minimun_characters} Zeichen, nicht zu gewöhnlich (z.B. 123456) und darf nicht Ihr Benutzername oder Ihre E-Mail-Adresse sein."
password_help: Mindestens %{minimun_characters} Zeichen, nicht zu gewöhnlich (z.B. 123456) und darf nicht Ihr Benutzername oder Ihre E-Mail-Adresse sein.
postal_code: Postleitzahl
sign_in: Anmelden
sign_up: Registrieren
Expand Down
6 changes: 6 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ en:
user_menu:
conversations: Conversations
notifications: Notifications
rack_attack:
too_many_requests:
message: Your connection has been slowed because server received too many requests.
time: 'You will be able to navigate on our website in :'
time_unit: seconds
title: Thank you for your participation on %{organization_name}
6 changes: 6 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,9 @@ fr:
user_menu:
conversations: Conversations
notifications: Notifications
rack_attack:
too_many_requests:
message: Il semblerait que vous fassiez trop de requetes sur notre serveur, votre connexion a ete ralentie.
time: 'Vous pourrez naviguer de nouveau sur notre plateforme dans :'
time_unit: secondes
title: Merci pour votre participation sur %{organization_name}
8 changes: 8 additions & 0 deletions config/secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@

default: &default
asset_host: <%= ENV["ASSET_HOST"] %>
decidim:
rack_attack:
enabled: <%= ENV["ENABLE_RACK_ATTACK"]&.to_i || 1 %>
fail2ban:
enabled: <%= ENV["RACK_ATTACK_FAIL2BAN"]&.to_i || 1 %>
throttle:
max_requests: <%= ENV["THROTTLING_MAX_REQUESTS"]&.to_i || 100 %>
period: <%= ENV["THROTTLING_PERIOD"]&.to_i || 60 %>
scaleway:
id: <%= ENV["SCALEWAY_ID"] %>
token: <%= ENV["SCALEWAY_TOKEN"] %>
Expand Down
2 changes: 2 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@
SocialShareButton.configure do |social_share_button|
social_share_button.allow_sites = %w(twitter facebook whatsapp_app whatsapp_web telegram)
end

allow(Rack::Attack).to receive(:enabled).and_return(false)
end
end