Add IP blocking against DNSBL at sign-up
This commit is contained in:
parent
481644ca7c
commit
8536e083f7
10 changed files with 242 additions and 1 deletions
|
@ -71,6 +71,7 @@ v 8.4.0 (unreleased)
|
|||
- Expose button to CI Lint tool on project builds page
|
||||
- Fix: Creator should be added as a master of the project on creation
|
||||
- Added X-GitLab-... headers to emails from CI and Email On Push services (Anton Baklanov)
|
||||
- Add IP check against DNSBLs at account sign-up
|
||||
|
||||
v 8.3.4
|
||||
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
|
||||
|
|
|
@ -74,6 +74,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
:metrics_timeout,
|
||||
:metrics_method_call_threshold,
|
||||
:metrics_sample_interval,
|
||||
:ip_blocking_enabled,
|
||||
:dnsbl_servers_list,
|
||||
:recaptcha_enabled,
|
||||
:recaptcha_site_key,
|
||||
:recaptcha_private_key,
|
||||
|
|
|
@ -8,6 +8,11 @@ class RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
def create
|
||||
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
|
||||
if Gitlab::IpCheck.new(request.remote_ip).spam?
|
||||
flash[:alert] = 'Could not create an account. This IP is listed for spam.'
|
||||
return render action: 'new'
|
||||
end
|
||||
|
||||
super
|
||||
else
|
||||
flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
# recaptcha_site_key :string
|
||||
# recaptcha_private_key :string
|
||||
# metrics_port :integer default(8089)
|
||||
# ip_blocking_enabled :boolean default(FALSE)
|
||||
# dns_blacklist_threshold :float default(0.33)
|
||||
#
|
||||
|
||||
class ApplicationSetting < ActiveRecord::Base
|
||||
|
|
|
@ -212,6 +212,22 @@
|
|||
|
||||
%fieldset
|
||||
%legend Spam and Anti-bot Protection
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
.checkbox
|
||||
= f.label :ip_blocking_enabled do
|
||||
= f.check_box :ip_blocking_enabled
|
||||
Enable IP check against blacklist at sign-up
|
||||
.help-block Helps preventing accounts creation from 'known spam sources'
|
||||
|
||||
.form-group
|
||||
= f.label :dnsbl_servers_list, class: 'control-label col-sm-2' do
|
||||
DNSBL servers list
|
||||
.col-sm-10
|
||||
= f.text_field :dnsbl_servers_list, class: 'form-control'
|
||||
.help-block
|
||||
Please enter DNSBL servers separated with comma
|
||||
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
.checkbox
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
class AddIpBlockingSettingsToApplicationSettings < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :application_settings, :ip_blocking_enabled, :boolean, default: false
|
||||
add_column :application_settings, :dnsbl_servers_list, :text
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20160119145451) do
|
||||
ActiveRecord::Schema.define(version: 20160120130905) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -62,6 +62,8 @@ ActiveRecord::Schema.define(version: 20160119145451) do
|
|||
t.string "recaptcha_private_key"
|
||||
t.integer "metrics_port", default: 8089
|
||||
t.integer "metrics_sample_interval", default: 15
|
||||
t.boolean "ip_blocking_enabled", default: false
|
||||
t.text "dnsbl_servers_list"
|
||||
end
|
||||
|
||||
create_table "audit_events", force: :cascade do |t|
|
||||
|
|
105
lib/dnsxl_check.rb
Normal file
105
lib/dnsxl_check.rb
Normal file
|
@ -0,0 +1,105 @@
|
|||
require 'resolv'
|
||||
|
||||
class DNSXLCheck
|
||||
|
||||
class Resolver
|
||||
def self.search(query)
|
||||
begin
|
||||
Resolv.getaddress(query)
|
||||
true
|
||||
rescue Resolv::ResolvError
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
IP_REGEXP = /\A(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\z/
|
||||
DEFAULT_THRESHOLD = 0.33
|
||||
|
||||
def self.create_from_list(list)
|
||||
dnsxl_check = DNSXLCheck.new
|
||||
|
||||
list.each do |entry|
|
||||
dnsxl_check.add_list(entry.domain, entry.weight)
|
||||
end
|
||||
|
||||
dnsxl_check
|
||||
end
|
||||
|
||||
def test(ip)
|
||||
if use_threshold?
|
||||
test_with_threshold(ip)
|
||||
else
|
||||
test_strict(ip)
|
||||
end
|
||||
end
|
||||
|
||||
def test_with_threshold(ip)
|
||||
return false if lists.empty?
|
||||
|
||||
search(ip)
|
||||
final_score >= threshold
|
||||
end
|
||||
|
||||
def test_strict(ip)
|
||||
return false if lists.empty?
|
||||
|
||||
search(ip)
|
||||
@score > 0
|
||||
end
|
||||
|
||||
def use_threshold=(value)
|
||||
@use_threshold = value == true
|
||||
end
|
||||
|
||||
def use_threshold?
|
||||
@use_threshold &&= true
|
||||
end
|
||||
|
||||
def threshold=(threshold)
|
||||
raise ArgumentError, "'threshold' value must be grather than 0 and less than or equal to 1" unless threshold > 0 && threshold <= 1
|
||||
@threshold = threshold
|
||||
end
|
||||
|
||||
def threshold
|
||||
@threshold ||= DEFAULT_THRESHOLD
|
||||
end
|
||||
|
||||
def add_list(domain, weight)
|
||||
@lists ||= []
|
||||
@lists << { domain: domain, weight: weight }
|
||||
end
|
||||
|
||||
def lists
|
||||
@lists ||= []
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search(ip)
|
||||
raise ArgumentError, "'ip' value must be in #{IP_REGEXP} format" unless ip.match(IP_REGEXP)
|
||||
|
||||
@score = 0
|
||||
|
||||
reversed = reverse_ip(ip)
|
||||
search_in_rbls(reversed)
|
||||
end
|
||||
|
||||
def reverse_ip(ip)
|
||||
ip.split('.').reverse.join('.')
|
||||
end
|
||||
|
||||
def search_in_rbls(reversed_ip)
|
||||
lists.each do |rbl|
|
||||
query = "#{reversed_ip}.#{rbl[:domain]}"
|
||||
@score += rbl[:weight] if Resolver.search(query)
|
||||
end
|
||||
end
|
||||
|
||||
def final_score
|
||||
weights = lists.map{ |rbl| rbl[:weight] }.reduce(:+).to_i
|
||||
return 0 if weights == 0
|
||||
|
||||
(@score.to_f / weights.to_f).round(2)
|
||||
end
|
||||
end
|
34
lib/gitlab/ip_check.rb
Normal file
34
lib/gitlab/ip_check.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
module Gitlab
|
||||
class IpCheck
|
||||
|
||||
def initialize(ip)
|
||||
@ip = ip
|
||||
|
||||
application_settings = ApplicationSetting.current
|
||||
@ip_blocking_enabled = application_settings.ip_blocking_enabled
|
||||
@dnsbl_servers_list = application_settings.dnsbl_servers_list
|
||||
end
|
||||
|
||||
def spam?
|
||||
@ip_blocking_enabled && blacklisted?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blacklisted?
|
||||
on_dns_blacklist?
|
||||
end
|
||||
|
||||
def on_dns_blacklist?
|
||||
dnsbl_check = DNSXLCheck.new
|
||||
prepare_dnsbl_list(dnsbl_check)
|
||||
dnsbl_check.test(@ip)
|
||||
end
|
||||
|
||||
def prepare_dnsbl_list(dnsbl_check)
|
||||
@dnsbl_servers_list.split(',').map(&:strip).reject(&:empty?).each do |domain|
|
||||
dnsbl_check.add_list(domain, 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
68
spec/lib/dnsxl_check_spec.rb
Normal file
68
spec/lib/dnsxl_check_spec.rb
Normal file
|
@ -0,0 +1,68 @@
|
|||
require 'spec_helper'
|
||||
require 'ostruct'
|
||||
|
||||
describe 'DNSXLCheck', lib: true, no_db: true do
|
||||
let(:spam_ip) { '127.0.0.2' }
|
||||
let(:no_spam_ip) { '127.0.0.3' }
|
||||
let(:invalid_ip) { 'a.b.c.d' }
|
||||
let!(:dnsxl_check) { DNSXLCheck.create_from_list([OpenStruct.new({ domain: 'test', weight: 1 })]) }
|
||||
|
||||
before(:context) do
|
||||
class DNSXLCheck::Resolver
|
||||
class << self
|
||||
alias_method :old_search, :search
|
||||
def search(query)
|
||||
return false if query.match(/always\.failing\.domain\z/)
|
||||
return true if query.match(/\A2\.0\.0\.127\./)
|
||||
return false if query.match(/\A3\.0\.0\.127\./)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#test' do
|
||||
before do
|
||||
dnsxl_check.threshold = 0.75
|
||||
dnsxl_check.add_list('always.failing.domain', 1)
|
||||
end
|
||||
|
||||
context 'when threshold is used' do
|
||||
before { dnsxl_check.use_threshold= true }
|
||||
|
||||
it { expect(dnsxl_check.test(spam_ip)).to be_falsey }
|
||||
end
|
||||
|
||||
context 'when threshold is not used' do
|
||||
before { dnsxl_check.use_threshold= false }
|
||||
|
||||
it { expect(dnsxl_check.test(spam_ip)).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#test_with_threshold' do
|
||||
it { expect{ dnsxl_check.test_with_threshold(invalid_ip) }.to raise_error(ArgumentError) }
|
||||
|
||||
it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_truthy }
|
||||
it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey }
|
||||
end
|
||||
|
||||
describe '#test_strict' do
|
||||
before do
|
||||
dnsxl_check.threshold = 1
|
||||
dnsxl_check.add_list('always.failing.domain', 1)
|
||||
end
|
||||
|
||||
it { expect{ dnsxl_check.test_strict(invalid_ip) }.to raise_error(ArgumentError) }
|
||||
|
||||
it { expect(dnsxl_check.test_with_threshold(spam_ip)).to be_falsey }
|
||||
it { expect(dnsxl_check.test_with_threshold(no_spam_ip)).to be_falsey }
|
||||
it { expect(dnsxl_check.test_strict(spam_ip)).to be_truthy }
|
||||
it { expect(dnsxl_check.test_strict(no_spam_ip)).to be_falsey }
|
||||
end
|
||||
|
||||
describe '#threshold=' do
|
||||
it { expect{ dnsxl_check.threshold = 0 }.to raise_error(ArgumentError) }
|
||||
it { expect{ dnsxl_check.threshold = 1.1 }.to raise_error(ArgumentError) }
|
||||
it { expect{ dnsxl_check.threshold = 0.5 }.not_to raise_error }
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue