From 62ea02740d2fff83d636eb659eb5f80dbf1bd888 Mon Sep 17 00:00:00 2001
From: Jacob Vosmaer <contact@jacobvosmaer.nl>
Date: Mon, 15 Dec 2014 18:47:26 +0100
Subject: [PATCH] Block Git HTTP Basic Auth after 10 failed attempts

---
 CHANGELOG                                     |  3 +++
 config/gitlab.yml.example                     | 11 ++++++++++
 config/initializers/1_settings.rb             |  9 ++++++++
 .../rack_attack_git_basic_auth.rb             | 10 +++++++++
 config/initializers/redis-store-fix-expiry.rb | 21 +++++++++++++++++++
 lib/gitlab/backend/grack_auth.rb              | 14 +++++++++++--
 6 files changed, 66 insertions(+), 2 deletions(-)
 create mode 100644 config/initializers/rack_attack_git_basic_auth.rb
 create mode 100644 config/initializers/redis-store-fix-expiry.rb

diff --git a/CHANGELOG b/CHANGELOG
index 2061237fb42..e4d180359b7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,6 @@
+v 7.7.0
+  - Block Git HTTP access after 10 failed authentication attempts
+
 v 7.6.0
   - Fork repository to groups
   - New rugged version
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 7b4c180fccc..b474063505f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -298,6 +298,17 @@ production: &base
     #   ![Company Logo](http://www.companydomain.com/logo.png)
     #   [Learn more about CompanyName](http://www.companydomain.com/)
 
+  rack_attack:
+    git_basic_auth:
+      # Limit the number of Git HTTP authentication attempts per IP
+      # maxretry: 10
+      #
+      # Reset the auth attempt counter per IP after 60 seconds
+      # findtime: 60
+      #
+      # Ban an IP for one hour (3600s) after too many auth attempts
+      # bantime: 3600
+
 development:
   <<: *base
 
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 27bb83784ba..4464d9d0001 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -171,6 +171,15 @@ Settings.satellites['timeout'] ||= 30
 #
 Settings['extra'] ||= Settingslogic.new({})
 
+#
+# Rack::Attack settings
+#
+Settings['rack_attack'] ||= Settingslogic.new({})
+Settings.rack_attack['git_basic_auth'] ||= Settingslogic.new({})
+Settings.rack_attack.git_basic_auth['maxretry'] ||= 10
+Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
+Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
+
 #
 # Testing settings
 #
diff --git a/config/initializers/rack_attack_git_basic_auth.rb b/config/initializers/rack_attack_git_basic_auth.rb
new file mode 100644
index 00000000000..2348768ff16
--- /dev/null
+++ b/config/initializers/rack_attack_git_basic_auth.rb
@@ -0,0 +1,10 @@
+unless Rails.env.test?
+  Rack::Attack.blacklist('Git HTTP Basic Auth') do |req|
+    Rack::Attack::Allow2Ban.filter(req.ip, Gitlab.config.rack_attack.git_basic_auth) do
+      # This block only gets run if the IP was not already banned.
+      # Return false, meaning that we do not see anything wrong with the
+      # request at this time
+      false
+    end
+  end
+end
diff --git a/config/initializers/redis-store-fix-expiry.rb b/config/initializers/redis-store-fix-expiry.rb
new file mode 100644
index 00000000000..dd27596cd0b
--- /dev/null
+++ b/config/initializers/redis-store-fix-expiry.rb
@@ -0,0 +1,21 @@
+# Monkey-patch Redis::Store to make 'setex' and 'expire' work with namespacing
+
+module Gitlab
+  class Redis
+    class Store
+      module Namespace
+        def setex(key, expires_in, value, options=nil)
+          namespace(key) { |key| super(key, expires_in, value) }
+        end
+
+        def expire(key, expires_in)
+          namespace(key) { |key| super(key, expires_in) }
+        end
+      end
+    end
+  end
+end
+
+Redis::Store.class_eval do
+  include Gitlab::Redis::Store::Namespace
+end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 762639414e0..ab5d2ef3da4 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -72,8 +72,18 @@ module Grack
     end
 
     def authenticate_user(login, password)
-      auth = Gitlab::Auth.new
-      auth.find(login, password)
+      user = Gitlab::Auth.new.find(login, password)
+      return user if user.present?
+
+      # At this point, we know the credentials were wrong. We let Rack::Attack
+      # know there was a failed authentication attempt from this IP
+      Rack::Attack::Allow2Ban.filter(@request.ip, Gitlab.config.rack_attack.git_basic_auth) do
+        # Return true, so that Allow2Ban increments the counter (stored in
+        # Rails.cache) for the IP
+        true
+      end
+
+      nil # No user was found
     end
 
     def authorized_request?
-- 
GitLab