[Kimchi-devel] [PATCH v2][Wok] Bug fix #147: Block authentication request after too many failures

Ramon Medeiros ramonn at linux.vnet.ibm.com
Wed Jan 18 11:55:52 UTC 2017


To prevent brute force attack, creates a mechanism to allow 3 tries
first. After that, a timeout will start and will be added 30 seconds for
each failed try in a row.

Signed-off-by: Ramon Medeiros <ramonn at linux.vnet.ibm.com>
---
Changes:

v2:
Set timeout by user, ip and session id. This will avoid trouble with 
users using the same ip, like NAT.

Only count as failed login, tries with less than 30 seconds.

Does not store all try: store last time try.

 src/wok/i18n.py          |  1 +
 src/wok/root.py          | 57 +++++++++++++++++++++++++++++++++++++++++++++---
 ui/js/src/wok.login.js   | 10 ++++++++-
 ui/pages/login.html.tmpl |  3 ++-
 4 files changed, 66 insertions(+), 5 deletions(-)

diff --git a/src/wok/i18n.py b/src/wok/i18n.py
index e454e31..21cc4ea 100644
--- a/src/wok/i18n.py
+++ b/src/wok/i18n.py
@@ -41,6 +41,7 @@ messages = {
     "WOKAUTH0001E": _("Authentication failed for user '%(username)s'. [Error code: %(code)s]"),
     "WOKAUTH0002E": _("You are not authorized to access Wok. Please, login first."),
     "WOKAUTH0003E": _("Specify %(item)s to login into Wok."),
+    "WOKAUTH0004E": _("You have failed to login in too much attempts. Please, wait for %(seconds)s seconds to try again."),
     "WOKAUTH0005E": _("Invalid LDAP configuration: %(item)s : %(value)s"),
 
     "WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"),
diff --git a/src/wok/root.py b/src/wok/root.py
index 080b7f0..4cabfdf 100644
--- a/src/wok/root.py
+++ b/src/wok/root.py
@@ -1,7 +1,7 @@
 #
 # Project Wok
 #
-# Copyright IBM Corp, 2015-2016
+# Copyright IBM Corp, 2015-2017
 #
 # Code derived from Project Kimchi
 #
@@ -21,7 +21,9 @@
 
 import cherrypy
 import json
+import re
 import os
+import time
 from distutils.version import LooseVersion
 
 from wok import auth
@@ -31,7 +33,7 @@ from wok.config import paths as wok_paths
 from wok.control import sub_nodes
 from wok.control.base import Resource
 from wok.control.utils import parse_request
-from wok.exception import MissingParameter
+from wok.exception import MissingParameter, UnauthorizedError
 from wok.reqlogger import log_request
 
 
@@ -48,7 +50,8 @@ class Root(Resource):
         super(Root, self).__init__(model)
         self._handled_error = ['error_page.400', 'error_page.404',
                                'error_page.405', 'error_page.406',
-                               'error_page.415', 'error_page.500']
+                               'error_page.415', 'error_page.500',
+                               'error_page.403']
 
         if not dev_env:
             self._cp_config = dict([(key, self.error_production_handler)
@@ -146,6 +149,7 @@ class WokRoot(Root):
         self.domain = 'wok'
         self.messages = messages
         self.extends = None
+        self.failed_logins = {}
 
         # set user log messages and make sure all parameters are present
         self.log_map = ROOT_REQUESTS
@@ -153,6 +157,13 @@ class WokRoot(Root):
 
     @cherrypy.expose
     def login(self, *args):
+        def _raise_timeout(user_id):
+            length = self.failed_logins[user_ip_sid]["count"]
+            timeout = (length - 3) * 30
+            details = e = UnauthorizedError("WOKAUTH0004E",
+                                            {"seconds": timeout})
+            log_request(code, params, details, method, 403)
+            raise cherrypy.HTTPError(403, e.message)
         details = None
         method = 'POST'
         code = self.getRequestMessage(method, 'login')
@@ -166,10 +177,50 @@ class WokRoot(Root):
             log_request(code, params, details, method, 400)
             raise cherrypy.HTTPError(400, e.message)
 
+        # get authentication info
+        remote_ip = cherrypy.request.remote.ip
+        session_id = str(cherrypy.session.originalid)
+        user_ip_sid = re.escape(username + remote_ip + session_id)
+
+        # check for repetly
+        count = self.failed_logins.get(user_ip_sid, {"count": 0}).get("count")
+        if count >= 3:
+
+                # verify if timeout is still valid
+                last_try = self.failed_logins[user_ip_sid]["time"]
+                if time.time() < (last_try + ((count - 3) * 30)):
+                    _raise_timeout(user_ip_sid)
+
         try:
             status = 200
             user_info = auth.login(username, password)
+
+            # user logged sucessfuly: reset counters
+            if self.failed_logins.get(user_ip_sid) != None:
+                self.failed_logins.remove(user_ip_sid)
         except cherrypy.HTTPError, e:
+
+            # store time and prevent too much tries
+            if self.failed_logins.get(user_ip_sid) == None:
+                self.failed_logins[user_ip_sid] = {"time": time.time(),
+                                                   "ip": remote_ip,
+                                                   "session_id": session_id,
+                                                   "username": username,
+                                                   "count": 1}
+            else:
+
+                # tries take more than 30 seconds between each one: do not
+                # increase count
+                if (time.time() -
+                        self.failed_logins[user_ip_sid]["time"]) < 30:
+
+                    self.failed_logins[user_ip_sid]["time"] = time.time()
+                    self.failed_logins[user_ip_sid]["count"] += 1
+
+            # more than 3 fails: raise error
+            if self.failed_logins[user_ip_sid]["count"] > 3:
+                _raise_timeout(user_ip_sid)
+
             status = e.status
             raise
         finally:
diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js
index fa2a98a..12c6880 100644
--- a/ui/js/src/wok.login.js
+++ b/ui/js/src/wok.login.js
@@ -1,7 +1,7 @@
 /*
  * Project Wok
  *
- * Copyright IBM Corp, 2015-2016
+ * Copyright IBM Corp, 2015-2017
  *
  * Code derived from Project Kimchi
  *
@@ -84,9 +84,17 @@ wok.login_main = function() {
             if (jqXHR.responseText == "") {
                 $("#messUserPass").hide();
                 $("#missServer").show();
+                $("#timeoutError").hide();
+            } else if ((jqXHR.responseJSON != undefined) &&
+                       ! (jqXHR.responseJSON["reason"] == undefined)) {
+                $("#messUserPass").hide();
+                $("#missServer").hide();
+                $("#timeoutError").html(jqXHR.responseJSON["reason"]);
+                $("#timeoutError").show();
             } else {
                 $("#missServer").hide();
                 $("#messUserPass").show();
+                $("#timeoutError").hide();
             }
             $("#messSession").hide();
             $("#logging").hide();
diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl
index f5a4b2d..d25910c 100644
--- a/ui/pages/login.html.tmpl
+++ b/ui/pages/login.html.tmpl
@@ -1,7 +1,7 @@
 #*
  * Project Wok
  *
- * Copyright IBM Corp, 2014-2016
+ * Copyright IBM Corp, 2014-2017
  *
  * Code derived from Project Kimchi
  *
@@ -107,6 +107,7 @@
                     <div id="messUserPass" class="alert alert-danger" style="display: none;">$_("The username or password you entered is incorrect. Please try again.")</div>
                     <div id="messSession" class="alert alert-danger" style="display: none;">$_("Session timeout, please re-login.")</div>
                     <div id="missServer" class="alert alert-danger" style="display: none;">$_("Server unreachable.")</div>
+                    <div id="timeoutError" class="alert alert-danger" style="display: none;">$_("Timeout error")</div>
                 </div>
                 <form id="form-login" class="form-horizontal" method="post">
                     <div class="form-group">
-- 
2.7.4



More information about the Kimchi-devel mailing list