đȘ đ« ProtĂ©ger sans ralentir : tests optimisĂ©s avec Rack Attack
Temps de lecture : 10 minutes
đš Cette Ă©dition est relativement longue, votre boĂźte mail risque de la tronquer. Je vous conseille de cliquer sur le titre ci-dessus pour lâouvrir dans votre navigateur web. âïž
Au programme aujourdâhui :
Protéger sans ralentir : tests optimisés avec Rack Attack par Ines
Temps de lecture : 10 minutes
Hello les petits Biscuits !
Bienvenue sur la 26Ăšme Ă©dition de Ruby Biscuit.
Vous ĂȘtes maintenant 530 abonnĂ©s đ„ł
Maintenant Ruby biscuit, câest aussi votre meilleur alliĂ© pour recruter des devs Ruby !
Si vous nâavez pas encore rejoint le club, RDV sur https://recrutement.rubybiscuit.fr
Bonne lecture.
Protéger sans ralentir : tests optimisés avec Rack Attack
En tant que dĂ©veloppeuse junior, lorsque j'ai tentĂ© d'implĂ©menter Rack Attack et surtout d'accĂ©lĂ©rer les tests que j'avais rĂ©digĂ©s, l'expĂ©rience s'est rĂ©vĂ©lĂ©e ĂȘtre un vĂ©ritable casse-tĂȘte. Il y a trĂšs peu d'informations lĂ -dessus sur internet et j'ai eu du mal Ă comprendre certains aspects de l'utilisation du cache de Rack Attack afin de pouvoir les mocker*. J'ai souhaitĂ© Ă©crire un article sur le sujet pour vous Ă©pargner ces difficultĂ©s.
* Mock : En programmation orientée objet, les mocks sont des objets simulés qui reproduisent le comportement d'objets réels de maniÚre contrÎlée.
Rack Attack est une gem en Ruby qui implémente un middleware. Elle est particuliÚrement utile pour protéger les applications Ruby en Rails contre certaines attaques :
par force brute : un attaquant essaye chaque combinaison possible de mot de passe pour un nom d'utilisateur/email, afin d'obtenir l'accÚs au service ciblé
par bourrage d'identifiants (credential stuffing) : un attaquant tente une liste d'identifiants compromis récupérés dans des fuites de données, toujours dans l'objectif d'accéder au service ciblé
par dĂ©ni de service (Denial of Service) : un attaquant submerge votre service de requĂȘtes jusqu'Ă ce que le traffic normal ne puisse ĂȘtre traitĂ©
Rack Attack se positionne entre votre application et les requĂȘtes HTTP brutes que celles-ci reçoit, et vous permet d'intercepter celles ci : dĂ©finir des rĂšgles de limitation de requĂȘtes, bloquer les IP suspectes ou abusives et mĂȘme mettre en place des listes blanches et noires. Rack Attack utilise moins de ressources par requĂȘte que votre application Rails, ce qui lui permet de ne pas ralentir les requĂȘtes tout en les filtrant.
Aujourd'hui, nous allons voir :
Un exemple simple de configuration Rack Attack
Des tests basiques
Une explication détaillée des interactions de Rack Attack avec le cache
Des tests ultra-rapides, indĂ©pendants du nombre de requĂȘtes autorisĂ©es
Quelques recommendations pour utiliser Rack Attack
Configuration
Pour simplifier cette configuration, nous allons nous concentrer sur les envois de formulaires publics, c'est-Ă -dire les requĂȘtes POST oĂč un formulaire est gĂ©nĂ©ralement envoyĂ© au serveur.
En effet, c'est en spammant le formulaire de login qu'une attaque par force brute peut ĂȘtre menĂ©e, et lors d'une attaque de type Denial of Service (DoS), les requĂȘtes POST sont bien plus coĂ»teuses pour le serveur que les requĂȘtes GET, puisqu'il ne s'agit pas seulement de rĂ©pondre Ă une demande, mais aussi de traiter les donnĂ©es reçues.
Installation
Ajouter la gem au Gemfile :
gem 'rack-attack'
Ensuite, exécutez la commande suivante pour installer la gem :
bundle install
Configuration Rack Attack
# config/initializers/rack_attack.rb | |
class Rack::Attack | |
# Configuration for Rack::Attack | |
BAN_TIME = 10.minutes.freeze | |
# GET requests | |
MAX_ATTEMPTS_GET = 20 | |
OBSERVATION_TIME_GET = 1.minute | |
PUBLIC_PATHS_GET = ["/", "/sign_in", "/sign_up"].freeze | |
# POST requests | |
PUBLIC_PATHS_POST = ["/sign_in", "/password", "/sign_up"].freeze | |
MAX_ATTEMPTS_POST = 25 | |
OBSERVATION_TIME_POST = 2.minutes | |
# Blocks GET requests | |
# Blocks for BAN_TIME after MAX_ATTEMPTS_GET requests in OBSERVATION_TIME_GET | |
Rack::Attack.blocklist("block abusive get requests") do |req| | |
Rack::Attack::Allow2Ban.filter("public-get:#{req.ip}", maxretry: MAX_ATTEMPTS_GET, | |
findtime: OBSERVATION_TIME_GET, | |
bantime: BAN_TIME) do | |
req.get? && PUBLIC_PATHS_GET.include?(req.path) | |
end | |
end | |
# Blocks POST requests | |
# Blocks for BAN_TIME after MAX_ATTEMPTS_POST requests in OBSERVATION_TIME_POST | |
Rack::Attack.blocklist("block abusive post requests") do |req| | |
Rack::Attack::Allow2Ban.filter("public-post:#{req.ip}", maxretry: MAX_ATTEMPTS_POST, | |
findtime: OBSERVATION_TIME_POST, | |
bantime: BAN_TIME) do | |
req.post? && PUBLIC_PATHS_POST.include?(req.path) | |
end | |
end | |
# Custom response for blocked requests | |
BLOCKED_HTTP_CODE = 503 | |
Rack::Attack.blocklisted_responder = lambda do |request| | |
[BLOCKED_HTTP_CODE, {}, []] | |
end | |
end |
Si vous n'avez jamais utilisé Rack Attack, pas de panique, on va détailler tout ça.
Créer la classe Rack::Attack
ParamĂštres
On définit tout d'abord les paramÚtres du filtre dans l'initializer :
Note : Dans cet article, j'utilise une limite de tentatives trÚs basse pour simplifier les explications. Je recommande d'utiliser en réalité une limite bien moins sévÚre : 50 tentatives en une minute par exemple.
SĂ©lecteurs
Les sĂ©lecteurs sont les caractĂ©ristiques des requĂȘtes qui passeront par les filtres. Ils dĂ©terminent quelles seront les requĂȘtes concernĂ©es par les limitations.
Puis on choisit le ou les sélecteurs. Nous sélectionnons donc la méthode : POST
et le chemin : PUBLIC_PATHS_POST
, on obtient le sélecteur suivant :
Dans cet exemple, seules les requĂȘtes vers l'un des chemins spĂ©cifiĂ©s et avec pour mĂ©thode post, seront limitĂ©es. Les requĂȘtes avec pour mĂ©thode get ne seront pas affectĂ©es.
Note : il existe de nombreux autres sélecteurs possibles (params, url, user-agent, content_length, body...). Vous trouverez ici la liste des clés utilisables pour request.env et ici leur implémentation (et traduction) par Rack.
Filtre
Avec notre nom, nos paramĂštres et nos sĂ©lecteurs nous pouvons construire le filtre par lequel passeront les requĂȘtes choisies.
Blocklist
Lorsque, pour une ip donnĂ©e, lâutilisateur atteint MAX_ATTEMPTS_POST
requĂȘtes POST
durant OBSERVATION_TIME_POST
sur lâensemble (ou une partie) des PUBLICS_PATH_POST
, nous souhaitons quâil soit bloquĂ© de lâensemble de lâapplication : nous plaçons donc notre filtre dans une blocklist inconditionnelle.
Note : Il est possible de nâappliquer le blocage quâĂ certains types de requĂȘtes de mĂȘme que pour les filtres.
Un second filtre pour l'exemple
Pour lâexemple et afin de mieux comprendre le fonctionnement de Rack Attack, nous allons dĂ©finir un autre filtre sur les requĂȘtes GET sur des chemins publics, dĂ©fini avant le filtre sur les requĂȘtes post dans notre fichier de configuration (cela aura son importance pour la suite).
Réponse personnalisée
Il est possible de dĂ©finir une rĂ©ponse HTTP personnalisĂ©e pour les requĂȘtes bloquĂ©es, la rĂ©ponse par dĂ©faut Ă©tant un 403 (Forbidden) pour les blocklists et 429 (Too many requests) pour les throttles.
J'ai fait le choix dans cette configuration d'un code 503 sans corps pour une raison trĂšs simple. Le code 503 est habituellement utilisĂ© par une plateforme pour signifier qu'elle est hors-service et ne peut traiter les requĂȘtes entrantes. C'est une façon de simuler un crash suite Ă une Ă©ventuelle attaque par DoS (un peu comme un opossum qui feindrait la mort pour dĂ©courager un prĂ©dateur peu minutieux).
Tester la configuration
Maintenant qu'on a correctement configuré Rack Attack, testons ce que l'on a implémenté.
L'idée est de vérifier que les IPs sont bien bloquées aprÚs un certain nombre de tentatives, puis que le déblocage se fait correctement une fois le délai de bannissement écoulé.
Nous commencerons par des tests simples et fonctionnels mais lents, puis nous verrons comment les améliorer.
Tests triviaux
PremiĂšre approche des tests de Rack Attack
require "rails_helper" | |
RSpec.describe "Rack::Attack", type: :request do | |
before(:all) do | |
Rack::Attack.enabled = true | |
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new | |
end | |
after(:all) do | |
Rack::Attack.enabled = false | |
end | |
before(:each) do | |
Rack::Attack.cache.store.clear # Avoid tests overlaps | |
end | |
let(:ban_time) { Rack::Attack::BAN_TIME } | |
let(:store) { Rack::Attack.cache.store } | |
let(:filters_list_regex) { /public-(post|get)/i } | |
let(:ip) { "1.2.3.4" } | |
# Tester que Rack Attack bloque seulement aprĂšs MAX_ATTEMPTS tentatives | |
shared_examples "rate limiting and IP blocking" do |http_method, path, max_attempts, filter| | |
it "only blocks IP after MAX_ATTEMPTS #{http_method} requests", aggregate_failures: true do | |
max_attempts.times do | |
public_send( | |
http_method, | |
path, | |
params: nil, | |
headers: { "REMOTE_ADDR" => ip } | |
) | |
expect(response.status).not_to eq(503) | |
end | |
public_send( | |
http_method, | |
path, | |
params: nil, | |
headers: { "REMOTE_ADDR" => ip } | |
) | |
expect(response.status).to eq(503) | |
end | |
end | |
# Tester que Rack Attack débloque aprÚs BAN TIME | |
shared_examples "IP unblocking" do |http_method, path, max_attempts, filter| | |
before do | |
(max_attempts + 1).times do | |
public_send( | |
http_method, | |
path, | |
params: nil, | |
headers: { "REMOTE_ADDR" => ip } | |
) | |
end | |
end | |
it "unblock IP after BAN_TIME for #{http_method} requests" do | |
travel_to(Time.current + ban_time + 1.minute) do | |
public_send( | |
http_method, path, | |
params: nil, | |
headers: { "REMOTE_ADDR" => ip } | |
) | |
expect(response.status).not_to eq(503) | |
end | |
end | |
end | |
describe "POST requests" do | |
context "when the ip is not banned" do | |
it_behaves_like "rate limiting and IP blocking", :post, Rack::Attack::PUBLIC_PATHS_POST.first, | |
Rack::Attack::MAX_ATTEMPTS_POST, "public-post" | |
end | |
context "when the ip is banned" do | |
it_behaves_like "IP unblocking", "post", Rack::Attack::PUBLIC_PATHS_POST.first, Rack::Attack::MAX_ATTEMPTS_POST, "public-post" | |
end | |
end | |
describe "GET requests" do | |
context "when the ip is not banned" do | |
it_behaves_like "rate limiting and IP blocking", :get, Rack::Attack::PUBLIC_PATHS_GET.first, | |
Rack::Attack::MAX_ATTEMPTS_GET, "public-get" | |
end | |
context "when the ip is banned" do | |
it_behaves_like "IP unblocking", "get", Rack::Attack::PUBLIC_PATHS_GET.first, Rack::Attack::MAX_ATTEMPTS_GET, "public-get" | |
end | |
end | |
end |
Limites des tests triviaux
De tels tests fonctionnent, et permettent effectivement de vĂ©rifier que la configuration Rack Attack a Ă©tĂ© correctement rĂ©alisĂ©e. Cependant, leur durĂ©e dĂ©pend du nombre de requĂȘtes autorisĂ©es par chaque filtre, puisquâil faut en simuler autant pour observer le blocage.
Ces tests peuvent donc sâavĂ©rer trĂšs longs lorsque lâon souhaite utiliser des pĂ©riodes dâobservation plus longues avec des nombres de tentatives maximales pour un filtre Ă©levĂ©es ou lorsquâil y a plusieurs filtres Ă tester (jusquâĂ 15 secondes pour un seul filtre testĂ© avec 100 requĂȘtes GET autorisĂ©es sur un ordinateur relativement puissant).
De la mĂȘme façon, le succĂšs du test dĂ©pend dans ce contexte de la vitesse d'exĂ©cution de l'environnement oĂč le test est lancĂ©. Si les requĂȘtes mettent trop de temps Ă s'exĂ©cuter on peut ne jamais atteindre la limite durant le temps d'observation. Et si l'on souhaite Ă©galement autoriser beaucoup de requĂȘtes sur une trĂšs courte pĂ©riode, le blocage est pratiquement impossible Ă tester : il est trĂšs difficile d'envoyer un nombre Ă©levĂ© de requĂȘtes en une pĂ©riode trĂšs courte avec les helper fournis par RSpec (ou rack-test).
Nous allons donc voir comment les appels au cache sont faits afin de mocker le nombre de requĂȘtes qui nous intĂ©resse (voire directement un blocage) et rendre ainsi le temps de test indĂ©pendant du nombre de requĂȘtes autorisĂ©es pour chaque filtre.
Comprendre les appels au cache
RackAttack utilise le cache pour stocker des informations sur les requĂȘtes effectuĂ©es par les utilisateurs ou les adresses IP afin d'optimiser les performances et d'appliquer des rĂšgles de limitation en coĂ»tant peu de ressources.
Afin d'illustrer les appels au cache effectués par Rack Attack pour surveiller le traffic et bloquer les clients abusifs, nous allons jouer un petit scénario. Mais d'abord, un petit point sur les clés du cache.
Génération des clés du cache
Nous observerons dans notre scénario deux types de clés utilisées par Rack Attack pour communiquer avec le cache.
Clés de blocklist
Les clés des blocklists permettent de vérifier (dans notre cas) qu'une ip donnée a été marquée dans le cache comme appartenant à une blocklist.
Clés de compteur (ou clés de filtre)
Les clĂ©s de compteur permettent Ă Rack Attack de garder la trace du nombre de requĂȘtes concernĂ©es par le filtre (et donc sĂ©lectionnĂ©es par le discriminant) rĂ©alisĂ©es pour une ip durant le temps d'observation.
Elle contient notamment une clĂ© timestamp. Cette clĂ© est calculĂ©e au moment oĂč la premiĂšre requĂȘte pour une ip durant la pĂ©riode d'observation est reçue :
clĂ© timestamp = date et heure de la premiĂšre requĂȘte / durĂ©e d'observation
La clé timestamp est donc propre à une ip, et à une période d'observation.
Comprendre les interactions avec le cache
Imaginons un petit scénario pour illustrer les appels au cache de Rack Attack.
Un jeune bot naïf et malveillant accÚde à votre plateforme dans l'intention de récupérer les identifiants de vos utilisateurs grùce à une attaque par force brute. Il arrive sur le chemin "/sign_in", et en moins d'une seconde, tente 11 combinaisons différentes nom d'utilisateur - mot de passe. Au bout de la 11 Úme, une page d'erreur s'affiche.
Le jeune bot est stoppé net : la plateforme semble hors-service. Il rentre bredouille. Pourtant, il y a actuellement de nombreux humains disciplinés qui y surfent en toute tranquillité. Comment est-ce possible ? Rembobinons.
PremiĂšre requĂȘte
Lorsque la premiĂšre requĂȘte est reçue par l'application (et interceptĂ©e par Rack Attack), Rack Attack consulte tout d'abord les blocklists. Il les consulte dans l'ordre dans lequel elles ont Ă©tĂ© dĂ©finies dans l'initializer.
Souvenez vous : on avait dĂ©fini dans notre configuration un filtre et une blocklist sur public-get avant celui sur public-post (dans l'initializer). Ătants dĂ©finis avant, ils seront consultĂ©s en premier, tout au long de ce scĂ©nario.
Les blocklists sont consultĂ©es et aucune entrĂ©e ne correspond Ă l'ip de notre bot malveillant, la requĂȘte sera donc autorisĂ©e, mais il faut quand mĂȘme suivre le nombre de requĂȘtes.
AprĂšs avoir consultĂ© les blocklists, Rack Attack identifie que la requĂȘte correspond Ă celles filtrĂ©es par le filtre public-post : il consulte donc le compteur associĂ©.
Cette fois encore, aucune entrée ne correspond à notre bot malveillant. Rack Attack génÚre donc un timestamp, puis ajoute une entrée qui contient l'ip du bot malveillant (1.2.3.4) :
[READ] Key: rack::attack:allow2ban:ban:public-get:1.2.3.4, Result: nil
[READ] Key: rack::attack:allow2ban:ban:public-post:1.2.3.4, Result: nil
[READ] Key: rack::attack:14376799:allow2ban:count:public-post:1.2.3.4, Result: nil
[WRITE] Key: rack::attack:14376799:allow2ban:count:public-post:1.2.3.4, Value: 1, Expires in: 19 seconds
La clĂ© timestamp sera ensuite conservĂ©e et utilisĂ©e pour suivre les requĂȘtes de cette ip (qui satisfont le discriminant du filtre), jusquâĂ la fin du temps dâobservation.
RequĂȘtes suivantes n < 10
Notre jeune bot naïf et malveillant ne le sait pas, mais il est désormais fiché. Il continue, en toute innocence, à spammer notre page de login, et rien ne semble sortir de l'ordinaire. Pourtant il est surveillé de prÚs.
Lorsqu'une nouvelle requĂȘte est envoyĂ©e, Rack Attack commence Ă©galement par consulter toutes les blocklists concernĂ©es, celles-ci ne contiennent toujours pas l'ip correspondante : le jeune bot peut continuer Ă spammer le login (pour le moment).
à chacune de ses tentatives, aprÚs la lecture des blocklists, le compteur de public-post est incrémenté, mais il n'atteint pas encore la valeur seuil attendue.
[READ] Key: rack::attack:14376799:allow2ban:count:public-post:1.2.3.4, Result: 2
[WRITE] Key: rack::attack:14376799:allow2ban:count:public-post:1.2.3.4, Value: 3, Expires in: 92 seconds
10Ăšme requĂȘte : la derniĂšre pour notre jeune bot
Lors de la dixiĂšme tentative, les blocklists sont Ă nouveaux consultĂ©es et ne contiennent toujours pas l'ip recherchĂ©e et laissent donc passer de nouveau cette requĂȘte. Finiront t'elles par servir Ă quelque chose ?
Puis, le compteur de public-post est consulté : il renvoie 9. Il est alors incrémenté pour atteindre 10 : le maximum autorisé. C'est à ce moment que les choses tournent mal pour le jeune bot malveillant. Un nouvel appel en écriture au cache est effectué : cette fois-ci, non pas au compteur, mais bien à la blocklist de public-post. Une entrée contenant son ip y est inscrite et sera conservée durant le BAN_TIME défini plus haut.
[WRITE] Key: rack::attack:allow2ban:ban:public-post:1.2.3.4, Value: 1, Expires in: 600 seconds
Note : Les clĂ©s de compteur servent exclusivement Ă suivre le nombre de requĂȘtes durant une pĂ©riode d'observation. La lecture d'une valeur supĂ©rieure ou Ă©gale Ă n-1 dĂ©clenchera l'Ă©criture d'un bannissement qui bloquera les requĂȘtes suivantes. Mais la lecture seule du compteur, mĂȘme si elle renvoie un nombre de requĂȘtes supĂ©rieur Ă celui autorisĂ©, ne permet pas de bloquer une requĂȘte.
11Ăšme requĂȘte :
Lors de la rĂ©ception de cette derniĂšre requĂȘte, Rack Attack consulte les blocklists : il consulte la blocklist de public-get sans succĂšs puis il consulte la blocklist de public-post : cette fois-ci, il y trouve l'ip correspondante. Rack Attack ne prend mĂȘme pas la peine de lire le compteur : cette fois-ci c'est cuit pour notre jeune bot, la requĂȘte ne passera pas. Rack Attack envoie un simple code HTTP 503.
Rappelez-vous, notre jeune bot malveillant est aussi naĂŻf : il pense que la plateforme est hors-service, et s'arrĂȘte lĂ . Il s'en va chercher une nouvelle victime.
Note : DĂšs que Rack Attack reçoit une rĂ©ponse positive d'une blocklist, il refuse la requĂȘte et s'arrĂȘte lĂ . Si l'ip du bot avait Ă©tĂ© dans la blocklist de public-get, la blocklist de public-post n'aurait mĂȘme pas Ă©tĂ© consultĂ©e pour cette requĂȘte.
On peut Ă©galement remarquer que le compteur n'est pas consultĂ© pour cette requĂȘte : il n'est d'ailleurs jamais bloquant. Peu importe la valeur renvoyĂ©e par le compteur, la requĂȘte ne sera pas bloquĂ©e sans Ă©criture dans la blocklist.
Tests rapides : simuler les appels au cache
Maintenant que nous avons compris la façon dont Rack Attack traque et banni les requĂȘtes abusives, nous pouvons mocker les appels et Ă©critures du cache des requĂȘtes et vĂ©rifier que le blocage (et le dĂ©blocage) sont correctement rĂ©alisĂ©s par Rack Attack.
Au lieu de simuler lâentiĂšretĂ© du processus, nous allons simplement nous insĂ©rer au niveau de la n-eme requĂȘte, et faire croire Ă Rack Attack quâil sâagit bien de la n-Ăšme requĂȘte et non de la premiĂšre.
Simuler n - 1 requĂȘtes, envoyer rĂ©ellement la n-Ăšme et observer le blocage
Nous allons mocker un appel au cache en lecture : au lieu de renvoyer nil (pas de requĂȘte observĂ©e) nous souhaitons qu'il renvoie 9 (dĂ©jĂ n-1 requĂȘtes observĂ©es) et expect les appels au cache en Ă©criture et leurs consĂ©quences dans nos tests. Les blocklists Ă©tant vides Ă ce stade, il nâest pas nĂ©cessaire de les mocker, elles renverront le rĂ©sultat attendu.
Note : Cependant, Ă©tant donnĂ© que nous mockons un appel au cache avec des arguments spĂ©cifiques, il faut Ă©galement expect un appel au cache aux blocklist et demander dâappeler lâoriginal pour ne pas avoir de message dâerreur.
Ensuite on envoie la (pseudo) 10Ăšme requĂȘte, celle-ci dĂ©clenche le bannissement. On envoie la 11Ăšme et on vĂ©rifie que lâon est banni.
Une fois implémenté, voilà le résultat :
Simuler un blocage, observer le déblocage
C'est bon, on sait dĂ©sormais que Rack Attack bloque correctement les requĂȘtes abusives. Il ne reste plus qu'Ă vĂ©rifier que le dĂ©blocage est correctement effectuĂ© aprĂšs BAN_TIME, et on a terminĂ©.
Ce que l'on souhaite maintenant, c'est s'insérer aprÚs la n-Úme tentative et le déclenchement du blocage.
On implémente cette logique :
On ajoute le setup et on obtient nos tests finaux :
require "rails_helper" | |
RSpec.describe "Rack::Attack", type: :request do | |
before(:all) do | |
Rack::Attack.enabled = true | |
Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new | |
end | |
after(:all) do | |
Rack::Attack.enabled = false | |
end | |
before(:each) do | |
Rack::Attack.cache.store.clear # Avoid tests overlaps | |
end | |
let(:ban_time) { Rack::Attack::BAN_TIME } | |
let(:store) { Rack::Attack.cache.store } | |
let(:filters_list_regex) { /public-(post|get)/i } | |
let(:ip) { "1.2.3.4" } | |
# Tester que Rack Attack bloque seulement aprĂšs MAX_ATTEMPTS tentatives | |
shared_examples "rate limiting and IP blocking" do |http_method, path, max_attempts, filter| | |
let(:read_count_key) { /rack::attack:\d+:allow2ban:count:#{filter}:#{ip}/ } | |
let(:read_ban_key) { /rack::attack:allow2ban:ban:#{filters_list_regex}:#{ip}/ } | |
it "only blocks IP after MAX_ATTEMPTS #{http_method} requests", aggregate_failures: true do | |
expect(store).to receive(:read).with(read_count_key, anything).and_return(max_attempts - 1) | |
expect(store).to receive(:read).with(read_ban_key).at_least(:once).and_call_original | |
public_send(http_method, path, params: nil, headers: { "REMOTE_ADDR" => ip }) | |
expect(response.status).not_to eq(503) | |
public_send(http_method, path, params: nil, headers: { "REMOTE_ADDR" => ip }) | |
expect(response.status).to eq(503) | |
end | |
end | |
let(:ip) { "1.2.3.4" } | |
let(:ban_time) { Rack::Attack::BAN_TIME } | |
# Tester que Rack Attack débloque bien aprÚs BAN_TIME | |
shared_examples "IP unblocking" do |http_method, path, filter| | |
let(:ban_key) { "rack::attack:allow2ban:ban:#{filter}:#{ip}" } | |
it "unblock IP after BAN_TIME for #{http_method} requests", aggregate_failures: true do | |
store.write(ban_key, 1, expires_in: ban_time) | |
public_send(http_method, path, params: nil, headers: { "REMOTE_ADDR" => ip }) | |
expect(response.status).to eq(503) | |
travel_to(Time.now + ban_time + 1.minute) do | |
public_send(http_method, path, params: nil, headers: { "REMOTE_ADDR" => ip }) | |
expect(response.status).not_to eq(503) | |
end | |
end | |
end | |
describe "POST requests" do | |
context "when the ip is not banned" do | |
it_behaves_like "rate limiting and IP blocking", | |
"post", | |
Rack::Attack::PUBLIC_PATHS_POST.first, | |
Rack::Attack::MAX_ATTEMPTS_POST, | |
"public-post" | |
end | |
context "when the ip is banned" do | |
it_behaves_like "IP unblocking", | |
"post", | |
Rack::Attack::PUBLIC_PATHS_POST.first, | |
"public-post" | |
end | |
end | |
describe "GET requests" do | |
context "when the ip is not banned" do | |
it_behaves_like "rate limiting and IP blocking", | |
"get", | |
Rack::Attack::PUBLIC_PATHS_GET.first, | |
Rack::Attack::MAX_ATTEMPTS_GET, | |
"public-get" | |
end | |
context "when the ip is banned" do | |
it_behaves_like "IP unblocking", | |
"get", | |
Rack::Attack::PUBLIC_PATHS_GET.first, | |
"public-get" | |
end | |
end | |
end |
Tadam ! DĂ©sormais, peu importe la configuration que vous choisissez (notamment des pĂ©riodes d'observation plus longues avec par consĂ©quent beaucoup de requĂȘtes autorisĂ©es) vos tests auront une durĂ©e minimale et invariable.
Quelques recommandations pour la fin
Bon, je vais quand mĂȘme vous laisser avec quelques recommandations utiles, notamment si vous souhaitez Ă©tendre la configuration proposĂ©e dans cette article Ă d'autres types de requĂȘtes.
Chaque filtre, throttle ou blocklist dĂ©fini dans l'initializer doit avoir un nom unique pour ĂȘtre correctement traitĂ© par Rack Attack.
Utiliser un cache séparé du cache qui gÚre les fonctionnalités de l'application, afin de ne pas surcharger ce dernier.
Eviter les filtres trop gĂ©nĂ©raux, couvrant trop de routes ou qui traquent abusivement les requĂȘtes GET : vous ralentiriez le traitement des requĂȘtes concernĂ©es pour un gain moindre.
DĂ©sactiver Rack Attack en environnement de test, et ne l'activer que pour le fichier de test Rack Attack.
Attention aux bots de rĂ©fĂ©rencement : adaptez votre nombre de tentatives autorisĂ©es, Ă©vitez les filtres trop sĂ©vĂšres sur les requĂȘtes GET ou utilisez un site diffĂ©rent pour la vitrine (rĂ©fĂ©rencement) et la logique application.
â Ines
Une mini remarque pour l'avoir subi plusieurs fois. Attention a quelle IP est envoyé a Rack::Attack. Dans certain environment derriere beaucoup de proxy, le RealIP est pas toujours la bonne. Rails s'est bien améliorer sur la detection, Mais cela peut toujours arriver.
Si par hasard l'IP d'un proxy est detecté, vous vous retrouver a bannir tous vos utilisateurs tres vite. Il faut donc bien monitorer le nombre de bannissement pour couper au plus vite le system. Potentiellement un circuitbreaker sur variable d'env est apprécié.