summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomasz Maczukin <tomasz@maczukin.pl>2016-01-20 13:16:34 +0100
committerTomasz Maczukin <tomasz@maczukin.pl>2016-01-20 15:28:19 +0100
commit8536e083f7b2d7ed77ecae83774d75f68d66e0b4 (patch)
tree116af03385a36c4a3b9e985d4f6cd7a690b6443a
parent481644ca7c9f763d4646ad557cc3bcf8f4f71816 (diff)
downloadgitlab-ce-8536e083f7b2d7ed77ecae83774d75f68d66e0b4.tar.gz
Add IP blocking against DNSBL at sign-upfeature/check-against-rbl-only
-rw-r--r--CHANGELOG1
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb5
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml16
-rw-r--r--db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb6
-rw-r--r--db/schema.rb4
-rw-r--r--lib/dnsxl_check.rb105
-rw-r--r--lib/gitlab/ip_check.rb34
-rw-r--r--spec/lib/dnsxl_check_spec.rb68
10 files changed, 242 insertions, 1 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 2e0eee52a59..a17f2bfef45 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -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)
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 91f7d78bd73..6df8e8e4cd8 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -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,
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index c48175a4c5a..5efdd613e79 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -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."
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 6c6c2468374..db208884aa6 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -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
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 83f6814d822..4f915818d8f 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -215,6 +215,22 @@
.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
= f.label :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
diff --git a/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb
new file mode 100644
index 00000000000..26606b10b54
--- /dev/null
+++ b/db/migrate/20160118232755_add_ip_blocking_settings_to_application_settings.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index dc7cb9f667f..1457259ebaf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -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|
diff --git a/lib/dnsxl_check.rb b/lib/dnsxl_check.rb
new file mode 100644
index 00000000000..1e506b2d9cb
--- /dev/null
+++ b/lib/dnsxl_check.rb
@@ -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
diff --git a/lib/gitlab/ip_check.rb b/lib/gitlab/ip_check.rb
new file mode 100644
index 00000000000..f2e9b50d225
--- /dev/null
+++ b/lib/gitlab/ip_check.rb
@@ -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
diff --git a/spec/lib/dnsxl_check_spec.rb b/spec/lib/dnsxl_check_spec.rb
new file mode 100644
index 00000000000..a35a1be0c90
--- /dev/null
+++ b/spec/lib/dnsxl_check_spec.rb
@@ -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