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
|
- Expose button to CI Lint tool on project builds page
|
||||||
- Fix: Creator should be added as a master of the project on creation
|
- 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)
|
- 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
|
v 8.3.4
|
||||||
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
|
- Use gitlab-workhorse 0.5.4 (fixes API routing bug)
|
||||||
|
|
|
@ -74,6 +74,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||||
:metrics_timeout,
|
:metrics_timeout,
|
||||||
:metrics_method_call_threshold,
|
:metrics_method_call_threshold,
|
||||||
:metrics_sample_interval,
|
:metrics_sample_interval,
|
||||||
|
:ip_blocking_enabled,
|
||||||
|
:dnsbl_servers_list,
|
||||||
:recaptcha_enabled,
|
:recaptcha_enabled,
|
||||||
:recaptcha_site_key,
|
:recaptcha_site_key,
|
||||||
:recaptcha_private_key,
|
:recaptcha_private_key,
|
||||||
|
|
|
@ -8,6 +8,11 @@ class RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
|
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
|
super
|
||||||
else
|
else
|
||||||
flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
|
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_site_key :string
|
||||||
# recaptcha_private_key :string
|
# recaptcha_private_key :string
|
||||||
# metrics_port :integer default(8089)
|
# metrics_port :integer default(8089)
|
||||||
|
# ip_blocking_enabled :boolean default(FALSE)
|
||||||
|
# dns_blacklist_threshold :float default(0.33)
|
||||||
#
|
#
|
||||||
|
|
||||||
class ApplicationSetting < ActiveRecord::Base
|
class ApplicationSetting < ActiveRecord::Base
|
||||||
|
|
|
@ -212,6 +212,22 @@
|
||||||
|
|
||||||
%fieldset
|
%fieldset
|
||||||
%legend Spam and Anti-bot Protection
|
%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
|
.form-group
|
||||||
.col-sm-offset-2.col-sm-10
|
.col-sm-offset-2.col-sm-10
|
||||||
.checkbox
|
.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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -62,6 +62,8 @@ ActiveRecord::Schema.define(version: 20160119145451) do
|
||||||
t.string "recaptcha_private_key"
|
t.string "recaptcha_private_key"
|
||||||
t.integer "metrics_port", default: 8089
|
t.integer "metrics_port", default: 8089
|
||||||
t.integer "metrics_sample_interval", default: 15
|
t.integer "metrics_sample_interval", default: 15
|
||||||
|
t.boolean "ip_blocking_enabled", default: false
|
||||||
|
t.text "dnsbl_servers_list"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "audit_events", force: :cascade do |t|
|
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