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

Aline Manera alinefm at linux.vnet.ibm.com
Tue Jan 24 12:43:49 UTC 2017


Hi Ramon,

I haven't tested your patch yet, but find some code comments below:

On 01/18/2017 09:55 AM, Ramon Medeiros wrote:
> 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>

I know it was already there, but we don't need to specify a div element 
for each error message.
Instead of that, there should be only one div to contain the error 
message and the html content properly filled in the JS. Something 
similar to what you did above.

So move the error messages contained in the HTML below to i18n.json.tmpl 
and use it to set the content of the single error message div when needed.


>                   </div>
>                   <form id="form-login" class="form-horizontal" method="post">
>                       <div class="form-group">



More information about the Kimchi-devel mailing list