feat: add in-memory rate limiting for login endpoint
Rate-limit POST /admin/login at 10 attempts per 60 seconds per IP. Stored in Volumen.login_attempts so tests can reset the counter.
This commit is contained in:
@@ -18,4 +18,11 @@ require_relative "volumen/users"
|
||||
# The Sinatra app (`Volumen::Server`) and the CLI (`Volumen::CLI`) are loaded
|
||||
# on demand to keep the core library light.
|
||||
module Volumen
|
||||
def self.login_attempts
|
||||
@login_attempts ||= {}
|
||||
end
|
||||
|
||||
def self.reset_login_attempts!
|
||||
@login_attempts = {}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,6 +13,9 @@ module Volumen
|
||||
# Sinatra application: the public JSON API at /api/volumen and the
|
||||
# server-rendered admin at /admin.
|
||||
class Server < Sinatra::Base
|
||||
MAX_LOGIN_ATTEMPTS = 10
|
||||
LOGIN_WINDOW = 60 # seconds
|
||||
|
||||
configure do
|
||||
set :show_exceptions, false
|
||||
set :raise_errors, false
|
||||
@@ -102,6 +105,7 @@ module Volumen
|
||||
|
||||
post "/admin/login" do
|
||||
check_csrf!
|
||||
check_login_rate_limit!
|
||||
user = users.authenticate(params["username"], params["password"])
|
||||
if user
|
||||
session[:user] = user.username
|
||||
@@ -387,6 +391,19 @@ module Volumen
|
||||
halt(403, "Invalid CSRF token")
|
||||
end
|
||||
|
||||
def check_login_rate_limit!
|
||||
ip = request.ip.to_s
|
||||
now = Time.now.to_f
|
||||
cutoff = now - LOGIN_WINDOW
|
||||
attempts = (Volumen.login_attempts[ip] ||= []).select { |t| t > cutoff }
|
||||
if attempts.length >= MAX_LOGIN_ATTEMPTS
|
||||
halt(429, "Too many login attempts. Please wait and try again.")
|
||||
end
|
||||
|
||||
attempts << now
|
||||
Volumen.login_attempts[ip] = attempts
|
||||
end
|
||||
|
||||
def render_settings(error: nil, notice: nil)
|
||||
@error = error
|
||||
@notice = notice
|
||||
|
||||
Reference in New Issue
Block a user