Skip to content

Commit

Permalink
Added IP banning class.
Browse files Browse the repository at this point in the history
When account request form submitted too quickly,
the IP address is internally banned. This commit
also includes a callback to a bash script that an
admin can customize to ban the IP at the firewall
level (or do anything else too).
  • Loading branch information
prioux committed Feb 5, 2024
1 parent aa98627 commit 9589660
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 5 deletions.
12 changes: 12 additions & 0 deletions BrainPortal/app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ApplicationController < ActionController::Base
helper_method :start_page_path

# These will be executed in order
before_action :check_for_banned_ip
before_action :check_account_validity
before_action :prepare_messages
before_action :adjust_system_time_zone
Expand Down Expand Up @@ -402,6 +403,17 @@ def eval_in_controller(mycontroller, options={}, &block) #:nodoc:
context.instance_eval(&block)
end

# If the IP address connecting to us was banned in the past,
# we just completely stop all activity right there with code 401
def check_for_banned_ip
req_ip = cbrain_request_remote_ip
is_banned = BannedIps.banned_ip?(req_ip)
return true if ! is_banned
Rails.logger.info "Requests from IP '#{req_ip}' are banned"
head :unauthorized
return false
end

end

# Patch: Load all models so single-table inheritance works properly.
Expand Down
8 changes: 8 additions & 0 deletions BrainPortal/app/controllers/signups_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ def create #:nodoc:
@signup.form_page = 'CBRAIN' # Just a keyword that IDs the signup page
@signup.generate_token

# Bulletproof code to extract the timestamp of the form in the
# obfuscated 'auth_spec' parameter
form_generated_at_s = params[:auth_spec].presence || Time.now.to_i.to_s
form_generated_at_int = form_generated_at_s.to_i rescue Time.now.to_i
form_generated_at = Time.at(form_generated_at_int)
# If form was generated less then 10 seconds ago, ban the IP address
return ban_ip("Signup form filled too quickly") if Time.now - form_generated_at < 10.0

unless can_edit?(@signup)
redirect_to login_path
return
Expand Down
1 change: 1 addition & 0 deletions BrainPortal/app/views/signups/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<%= form_for @signup, :url => {:action => (@signup.new_record? ? "create" : "update"), :id => @signup.id} do |f| %>
<%= error_messages_for @signup %>
<%= hidden_field_tag :auth_spec, Time.now.to_i.to_s %>

<div class="generalbox">
<p class="warning"> Fields with an asterisk (*) are mandatory.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

require 'readline'
require 'reline' # Readline.get_screen_size fails me

# We need some sort of constant to refer to the console's
Expand Down Expand Up @@ -58,7 +59,7 @@ def initialize(bourreaux_list = Bourreau.order(:id).all, term_width = nil)
@bourreaux = bourreaux_list
@width = term_width
if term_width.blank? || term_width.to_i < 1
_,numcols = Reline.get_screen_size rescue [25,120]
_,numcols = Readline.get_screen_size rescue [25,120]
@width = numcols
end
@selected = {}
Expand Down Expand Up @@ -115,7 +116,7 @@ def interactive_control(initial_command = nil)
OPERATIONS

userinput = initial_command.presence
userinput ||= Reline.readline("Do something (h for help): ",false)
userinput ||= Readline.readline("Do something (h for help): ",false)
userinput = "Q" if userinput.nil?
inputkeywords = userinput.downcase.split(/\W+/).map(&:presence).compact

Expand All @@ -126,7 +127,7 @@ def interactive_control(initial_command = nil)
end
puts ""
if dowait && initial_command.blank?
Reline.readline("Press RETURN to continue: ",false)
Readline.readline("Press RETURN to continue: ",false)
puts ""
end
initial_command = nil
Expand Down Expand Up @@ -359,7 +360,7 @@ def process_user_letter(letter) #:nodoc:
puts " * @r@ will be substituted by the Bourreau's RAILS root path"
puts " * @d@ will be substituted by the Bourreau's DP cache dir path"
puts " * @g@ will be substituted by the Bourreau's gridshare dir path"
comm = Reline.readline("Bash command: ")
comm = Readline.readline("Bash command: ")
bash_command_on_bourreaux(comm)
return true
end
Expand Down
61 changes: 61 additions & 0 deletions BrainPortal/lib/banned_ips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@

#
# CBRAIN Project
#
# Copyright (C) 2008-2024
# The Royal Institution for the Advancement of Learning
# McGill University
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#

# Utility class to persistently manage a list
# of banned IP addresses within a Rails app.
# All methods are class methods.
class BannedIps

MAIN_LIST_KEY = "banned_ips_array"
BAN_PREFIX = "ban_ip_"
BAN_TIME = 2.weeks

# Return an array of the currently banned IPs
def self.banned_ips
list = Rails.cache.fetch(MAIN_LIST_KEY, :expires_in => BAN_TIME) { [] }
list2 = list.select { |ip| banned_ip?(ip) }
return list if list.size == list2.size
Rails.cache.write(MAIN_LIST_KEY, list2, :expires_in => BAN_TIME)
return list2
end

# Add an IP to the ban list
def self.ban_ip(ip, time = BAN_TIME)
current_list = banned_ips
current_list |= [ ip ]
current_list.sort!
Rails.cache.write(MAIN_LIST_KEY, current_list, :expires_in => time)
Rails.cache.write("#{BAN_PREFIX}#{ip}",true, :expires_in => time)
time
end

# Remove an IP to the ban list
def self.unban_ip(ip)
Rails.cache.delete("#{BAN_PREFIX}#{ip}") # true/nil
end

# Query an IP and return true if it is banned
def self.banned_ip?(ip)
Rails.cache.fetch("#{BAN_PREFIX}#{ip}") # true/nil
end

end
18 changes: 17 additions & 1 deletion BrainPortal/lib/request_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module RequestHelpers

def self.included(includer) #:nodoc:
includer.class_eval do
helper_method :cbrain_request_remote_ip
helper_method :cbrain_request_remote_ip, :ban_ip
end
end

Expand All @@ -50,5 +50,21 @@ def cbrain_request_remote_ip
@_remote_ip
end

# A controller method can instantaneously add the
# current client to the list of banned IP addresses;
# this method also sets the return code to 401, as
# will all future requests from that client. See
# also the ApplicationController method +check_for_banned_ip+
def ban_ip(message)
req_ip = cbrain_request_remote_ip
Rails.logger.info("Banning IP #{req_ip}: #{message}")
if req_ip != '127.0.0.1' # need to check for IPv6 too eventually
BannedIps.ban_ip(req_ip)
system("#{Rails.root}/vendor/cbrain/bin/ban_ip", req_ip.to_s)
end
head :unauthorized
false
end

end

22 changes: 22 additions & 0 deletions BrainPortal/vendor/cbrain/bin/ban_ip
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash

# This script can be adjusted by an administrator
# to provide a mechanism to ban an IP address.
#
# This script will receive a single argument,
# an IP address.
#
# Currently, CBRAIN will invoke it if a signup
# request form was posted too quickly (less than
# 10 seconds after the form was generated).

ip_to_ban="$1"

# Special safety check to never ban the local IP
if test "X$ip_to_ban" = "X127.0.0.1" ; then
echo "$0 not banning IP '$ip_to_ban'"
exit 0
fi

echo "$0 invoked to ban IP '$ip_to_ban'"

0 comments on commit 9589660

Please sign in to comment.