[PATCH v3] [Wok] Bug fix #147: Block authentication request after too many failures

From: Ramon Medeiros <ramonn@linux.vnet.ibm.com> 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@linux.vnet.ibm.com> --- Changes: v3: Improve error handling on login page v2: Set timeout by user, ip and session id. This will avoid trouble with users using the same ip, like NAT. src/wok/i18n.py | 2 ++ src/wok/root.py | 66 ++++++++++++++++++++++++++++++++++++++++++++---- ui/js/src/wok.login.js | 21 +++++++++------ ui/pages/i18n.json.tmpl | 5 +++- ui/pages/login.html.tmpl | 6 ++--- 5 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/wok/i18n.py b/src/wok/i18n.py index e454e31..7d595b8 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -41,7 +41,9 @@ 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"), + "WOKAUTH0006E": _("The username or password you entered is incorrect. Please try again."), "WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"), "WOKLOG0002E": _("Creation of log file failed: %(err)s"), diff --git a/src/wok/root.py b/src/wok/root.py index 080b7f0..d314d25 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', 'error_page.401'] 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') @@ -161,17 +172,62 @@ class WokRoot(Root): params = parse_request() username = params['username'] password = params['password'] + + # no data passed: raise error + if len(username + password) == 0: + raise KeyError + except KeyError, item: details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)}) 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: - status = e.status - raise + + # 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) + + details = e = MissingParameter('WOKAUTH0006E') + raise cherrypy.HTTPError(401, e.message) finally: log_request(code, params, details, method, status) diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js index fa2a98a..d9daacf 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 * @@ -19,6 +19,10 @@ */ wok.login_main = function() { "use strict"; + var i18n; + wok.getI18n(function(i18nObj){ + i18n = i18nObj; + }, false, "i18n.json", true); // verify if language is available var selectedLanguage = wok.lang.get(); @@ -50,7 +54,8 @@ wok.login_main = function() { var query = window.location.search; var error = /.*error=(.*?)(&|$)/g.exec(query); if (error && error[1] === "sessionTimeout") { - $("#messSession").show(); + $("#errorArea").html(i18n["WOKAUT0001E"]); + $("#errorArea").show(); } var userNameBox = $('#username'); @@ -82,13 +87,13 @@ wok.login_main = function() { window.location.replace(window.location.pathname.replace(/\/+login.html/, '') + next_url); }, function(jqXHR, textStatus, errorThrown) { if (jqXHR.responseText == "") { - $("#messUserPass").hide(); - $("#missServer").show(); - } else { - $("#missServer").hide(); - $("#messUserPass").show(); + $("#errorArea").html(i18n["WOKAUT0002E"]); + $("#errorArea").show(); + } else if ((jqXHR.responseJSON != undefined) && + ! (jqXHR.responseJSON["reason"] == undefined)) { + $("#errorArea").html(jqXHR.responseJSON["reason"]); + $("#errorArea").show(); } - $("#messSession").hide(); $("#logging").hide(); $("#login").show(); }); diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index ba29532..4329ad0 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -1,7 +1,7 @@ #* * Project Wok * - * Copyright IBM Corp, 2014-2016 + * Copyright IBM Corp, 2014-2017 * * Code derived from Project Kimchi * @@ -39,6 +39,9 @@ "WOKHOST6001M": "$_("Max:")", + "WOKAUT0001E": "$_("Session timeout, please re-login.")", + "WOKAUT0002E": "$_("Server unreachable")", + "WOKSETT0001M": "$_("Application")", "WOKSETT0002M": "$_("User")", "WOKSETT0003M": "$_("Request")", diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index f5a4b2d..6f967cf 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 * @@ -104,9 +104,7 @@ <div class="container"> <div id="login-window" class="login-area row"> <div class="err-area"> - <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="errorArea" class="alert alert-danger" style="display: none;"></div> </div> <form id="form-login" class="form-horizontal" method="post"> <div class="form-group"> -- 2.10.1 (Apple Git-78)

On 01/24/2017 06:12 PM, ramonn@linux.vnet.ibm.com wrote:
From: Ramon Medeiros <ramonn@linux.vnet.ibm.com>
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@linux.vnet.ibm.com> --- Changes:
v3: Improve error handling on login page
v2: Set timeout by user, ip and session id. This will avoid trouble with users using the same ip, like NAT.
src/wok/i18n.py | 2 ++ src/wok/root.py | 66 ++++++++++++++++++++++++++++++++++++++++++++---- ui/js/src/wok.login.js | 21 +++++++++------ ui/pages/i18n.json.tmpl | 5 +++- ui/pages/login.html.tmpl | 6 ++--- 5 files changed, 82 insertions(+), 18 deletions(-)
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index e454e31..7d595b8 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -41,7 +41,9 @@ 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"), + "WOKAUTH0006E": _("The username or password you entered is incorrect. Please try again."),
"WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"), "WOKLOG0002E": _("Creation of log file failed: %(err)s"), diff --git a/src/wok/root.py b/src/wok/root.py index 080b7f0..d314d25 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', 'error_page.401']
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') @@ -161,17 +172,62 @@ class WokRoot(Root): params = parse_request() username = params['username'] password = params['password'] +
It would be good to use strip() around username and password to avoid multiple white spaces.
+ # no data passed: raise error + if len(username + password) == 0: + raise KeyError +
What is this for? You are summing 'username' and 'password' and get the length of it. I am not sure is that what you want. If it is to identify a missing parameter you should check them individually. As username AND password should be a string with length != 0
except KeyError, item: details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)}) 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)
else: # remove the entry from the dict
+ 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: - status = e.status - raise + + # 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) +
+ details = e = MissingParameter('WOKAUTH0006E') + raise cherrypy.HTTPError(401, e.message) finally: log_request(code, params, details, method, status)
diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js index fa2a98a..d9daacf 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 * @@ -19,6 +19,10 @@ */ wok.login_main = function() { "use strict"; + var i18n; + wok.getI18n(function(i18nObj){ + i18n = i18nObj; + }, false, "i18n.json", true);
// verify if language is available var selectedLanguage = wok.lang.get(); @@ -50,7 +54,8 @@ wok.login_main = function() { var query = window.location.search; var error = /.*error=(.*?)(&|$)/g.exec(query); if (error && error[1] === "sessionTimeout") { - $("#messSession").show(); + $("#errorArea").html(i18n["WOKAUT0001E"]); + $("#errorArea").show(); }
var userNameBox = $('#username'); @@ -82,13 +87,13 @@ wok.login_main = function() { window.location.replace(window.location.pathname.replace(/\/+login.html/, '') + next_url); }, function(jqXHR, textStatus, errorThrown) { if (jqXHR.responseText == "") { - $("#messUserPass").hide(); - $("#missServer").show(); - } else { - $("#missServer").hide(); - $("#messUserPass").show(); + $("#errorArea").html(i18n["WOKAUT0002E"]); + $("#errorArea").show(); + } else if ((jqXHR.responseJSON != undefined) && + ! (jqXHR.responseJSON["reason"] == undefined)) { + $("#errorArea").html(jqXHR.responseJSON["reason"]); + $("#errorArea").show(); } - $("#messSession").hide(); $("#logging").hide(); $("#login").show(); }); diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index ba29532..4329ad0 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -1,7 +1,7 @@ #* * Project Wok * - * Copyright IBM Corp, 2014-2016 + * Copyright IBM Corp, 2014-2017 * * Code derived from Project Kimchi * @@ -39,6 +39,9 @@
"WOKHOST6001M": "$_("Max:")",
+ "WOKAUT0001E": "$_("Session timeout, please re-login.")", + "WOKAUT0002E": "$_("Server unreachable")", + "WOKSETT0001M": "$_("Application")", "WOKSETT0002M": "$_("User")", "WOKSETT0003M": "$_("Request")", diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index f5a4b2d..6f967cf 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 * @@ -104,9 +104,7 @@ <div class="container"> <div id="login-window" class="login-area row"> <div class="err-area"> - <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="errorArea" class="alert alert-danger" style="display: none;"></div> </div> <form id="form-login" class="form-horizontal" method="post"> <div class="form-group">

On 1/25/17 3:46 PM, Aline Manera wrote:
On 01/24/2017 06:12 PM, ramonn@linux.vnet.ibm.com wrote:
From: Ramon Medeiros <ramonn@linux.vnet.ibm.com>
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@linux.vnet.ibm.com> --- Changes:
v3: Improve error handling on login page
v2: Set timeout by user, ip and session id. This will avoid trouble with users using the same ip, like NAT.
src/wok/i18n.py | 2 ++ src/wok/root.py | 66 ++++++++++++++++++++++++++++++++++++++++++++---- ui/js/src/wok.login.js | 21 +++++++++------ ui/pages/i18n.json.tmpl | 5 +++- ui/pages/login.html.tmpl | 6 ++--- 5 files changed, 82 insertions(+), 18 deletions(-)
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index e454e31..7d595b8 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -41,7 +41,9 @@ 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"), + "WOKAUTH0006E": _("The username or password you entered is incorrect. Please try again."),
"WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"), "WOKLOG0002E": _("Creation of log file failed: %(err)s"), diff --git a/src/wok/root.py b/src/wok/root.py index 080b7f0..d314d25 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', 'error_page.401']
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') @@ -161,17 +172,62 @@ class WokRoot(Root): params = parse_request() username = params['username'] password = params['password'] +
It would be good to use strip() around username and password to avoid multiple white spaces.
+ # no data passed: raise error + if len(username + password) == 0: + raise KeyError +
What is this for? You are summing 'username' and 'password' and get the length of it. I am not sure is that what you want.
If it is to identify a missing parameter you should check them individually. As username AND password should be a string with length != 0
Well, when parse_request run, it never get a missing parameters, so i need to check if the var is empty. I will check each one alone.
except KeyError, item: details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)}) 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)
else: # remove the entry from the dict
ok.
+ 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: - status = e.status - raise + + # 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) +
+ details = e = MissingParameter('WOKAUTH0006E') + raise cherrypy.HTTPError(401, e.message) finally: log_request(code, params, details, method, status)
diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js index fa2a98a..d9daacf 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 * @@ -19,6 +19,10 @@ */ wok.login_main = function() { "use strict"; + var i18n; + wok.getI18n(function(i18nObj){ + i18n = i18nObj; + }, false, "i18n.json", true);
// verify if language is available var selectedLanguage = wok.lang.get(); @@ -50,7 +54,8 @@ wok.login_main = function() { var query = window.location.search; var error = /.*error=(.*?)(&|$)/g.exec(query); if (error && error[1] === "sessionTimeout") { - $("#messSession").show(); + $("#errorArea").html(i18n["WOKAUT0001E"]); + $("#errorArea").show(); }
var userNameBox = $('#username'); @@ -82,13 +87,13 @@ wok.login_main = function() { window.location.replace(window.location.pathname.replace(/\/+login.html/, '') + next_url); }, function(jqXHR, textStatus, errorThrown) { if (jqXHR.responseText == "") { - $("#messUserPass").hide(); - $("#missServer").show(); - } else { - $("#missServer").hide(); - $("#messUserPass").show(); + $("#errorArea").html(i18n["WOKAUT0002E"]); + $("#errorArea").show(); + } else if ((jqXHR.responseJSON != undefined) && + ! (jqXHR.responseJSON["reason"] == undefined)) { + $("#errorArea").html(jqXHR.responseJSON["reason"]); + $("#errorArea").show(); } - $("#messSession").hide(); $("#logging").hide(); $("#login").show(); }); diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index ba29532..4329ad0 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -1,7 +1,7 @@ #* * Project Wok * - * Copyright IBM Corp, 2014-2016 + * Copyright IBM Corp, 2014-2017 * * Code derived from Project Kimchi * @@ -39,6 +39,9 @@
"WOKHOST6001M": "$_("Max:")",
+ "WOKAUT0001E": "$_("Session timeout, please re-login.")", + "WOKAUT0002E": "$_("Server unreachable")", + "WOKSETT0001M": "$_("Application")", "WOKSETT0002M": "$_("User")", "WOKSETT0003M": "$_("Request")", diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index f5a4b2d..6f967cf 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 * @@ -104,9 +104,7 @@ <div class="container"> <div id="login-window" class="login-area row"> <div class="err-area"> - <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="errorArea" class="alert alert-danger" style="display: none;"></div> </div> <form id="form-login" class="form-horizontal" method="post"> <div class="form-group">

On 01/26/2017 08:42 AM, Ramon Medeiros wrote:
On 1/25/17 3:46 PM, Aline Manera wrote:
On 01/24/2017 06:12 PM, ramonn@linux.vnet.ibm.com wrote:
From: Ramon Medeiros <ramonn@linux.vnet.ibm.com>
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@linux.vnet.ibm.com> --- Changes:
v3: Improve error handling on login page
v2: Set timeout by user, ip and session id. This will avoid trouble with users using the same ip, like NAT.
src/wok/i18n.py | 2 ++ src/wok/root.py | 66 ++++++++++++++++++++++++++++++++++++++++++++---- ui/js/src/wok.login.js | 21 +++++++++------ ui/pages/i18n.json.tmpl | 5 +++- ui/pages/login.html.tmpl | 6 ++--- 5 files changed, 82 insertions(+), 18 deletions(-)
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index e454e31..7d595b8 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -41,7 +41,9 @@ 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"), + "WOKAUTH0006E": _("The username or password you entered is incorrect. Please try again."),
"WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"), "WOKLOG0002E": _("Creation of log file failed: %(err)s"), diff --git a/src/wok/root.py b/src/wok/root.py index 080b7f0..d314d25 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', 'error_page.401']
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') @@ -161,17 +172,62 @@ class WokRoot(Root): params = parse_request() username = params['username'] password = params['password'] +
It would be good to use strip() around username and password to avoid multiple white spaces.
+ # no data passed: raise error + if len(username + password) == 0: + raise KeyError +
What is this for? You are summing 'username' and 'password' and get the length of it. I am not sure is that what you want.
If it is to identify a missing parameter you should check them individually. As username AND password should be a string with length != 0
Well,
when parse_request run, it never get a missing parameters, so i need to check if the var is empty. I will check each one alone.
Hrm... could you point me the code on where it is done? From what I remember, the request parameters validation is done through API.json on which each parameter is specified with a required flag. I don't remember we have it for login request. Do we have? Otherwise, there is no way to control knows which parameters are required or not. Maybe it is time to add a entry for login on API.json to make username and password required fields and non-empty ones. And then you make sure to have only valid values when the request gets to backend (model) code.
except KeyError, item: details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)}) 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)
else: # remove the entry from the dict
ok.
+ 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: - status = e.status - raise + + # 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) +
+ details = e = MissingParameter('WOKAUTH0006E') + raise cherrypy.HTTPError(401, e.message) finally: log_request(code, params, details, method, status)
diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js index fa2a98a..d9daacf 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 * @@ -19,6 +19,10 @@ */ wok.login_main = function() { "use strict"; + var i18n; + wok.getI18n(function(i18nObj){ + i18n = i18nObj; + }, false, "i18n.json", true);
// verify if language is available var selectedLanguage = wok.lang.get(); @@ -50,7 +54,8 @@ wok.login_main = function() { var query = window.location.search; var error = /.*error=(.*?)(&|$)/g.exec(query); if (error && error[1] === "sessionTimeout") { - $("#messSession").show(); + $("#errorArea").html(i18n["WOKAUT0001E"]); + $("#errorArea").show(); }
var userNameBox = $('#username'); @@ -82,13 +87,13 @@ wok.login_main = function() { window.location.replace(window.location.pathname.replace(/\/+login.html/, '') + next_url); }, function(jqXHR, textStatus, errorThrown) { if (jqXHR.responseText == "") { - $("#messUserPass").hide(); - $("#missServer").show(); - } else { - $("#missServer").hide(); - $("#messUserPass").show(); + $("#errorArea").html(i18n["WOKAUT0002E"]); + $("#errorArea").show(); + } else if ((jqXHR.responseJSON != undefined) && + ! (jqXHR.responseJSON["reason"] == undefined)) { + $("#errorArea").html(jqXHR.responseJSON["reason"]); + $("#errorArea").show(); } - $("#messSession").hide(); $("#logging").hide(); $("#login").show(); }); diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index ba29532..4329ad0 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -1,7 +1,7 @@ #* * Project Wok * - * Copyright IBM Corp, 2014-2016 + * Copyright IBM Corp, 2014-2017 * * Code derived from Project Kimchi * @@ -39,6 +39,9 @@
"WOKHOST6001M": "$_("Max:")",
+ "WOKAUT0001E": "$_("Session timeout, please re-login.")", + "WOKAUT0002E": "$_("Server unreachable")", + "WOKSETT0001M": "$_("Application")", "WOKSETT0002M": "$_("User")", "WOKSETT0003M": "$_("Request")", diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index f5a4b2d..6f967cf 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 * @@ -104,9 +104,7 @@ <div class="container"> <div id="login-window" class="login-area row"> <div class="err-area"> - <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="errorArea" class="alert alert-danger" style="display: none;"></div> </div> <form id="form-login" class="form-horizontal" method="post"> <div class="form-group">

Just start the discussion about JSON validation here: http://lists.ovirt.org/pipermail/kimchi-devel/2017-January/017435.html On 1/26/17 9:04 AM, Aline Manera wrote:
On 01/26/2017 08:42 AM, Ramon Medeiros wrote:
On 1/25/17 3:46 PM, Aline Manera wrote:
On 01/24/2017 06:12 PM, ramonn@linux.vnet.ibm.com wrote:
From: Ramon Medeiros <ramonn@linux.vnet.ibm.com>
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@linux.vnet.ibm.com> --- Changes:
v3: Improve error handling on login page
v2: Set timeout by user, ip and session id. This will avoid trouble with users using the same ip, like NAT.
src/wok/i18n.py | 2 ++ src/wok/root.py | 66 ++++++++++++++++++++++++++++++++++++++++++++---- ui/js/src/wok.login.js | 21 +++++++++------ ui/pages/i18n.json.tmpl | 5 +++- ui/pages/login.html.tmpl | 6 ++--- 5 files changed, 82 insertions(+), 18 deletions(-)
diff --git a/src/wok/i18n.py b/src/wok/i18n.py index e454e31..7d595b8 100644 --- a/src/wok/i18n.py +++ b/src/wok/i18n.py @@ -41,7 +41,9 @@ 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"), + "WOKAUTH0006E": _("The username or password you entered is incorrect. Please try again."),
"WOKLOG0001E": _("Invalid filter parameter. Filter parameters allowed: %(filters)s"), "WOKLOG0002E": _("Creation of log file failed: %(err)s"), diff --git a/src/wok/root.py b/src/wok/root.py index 080b7f0..d314d25 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', 'error_page.401']
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') @@ -161,17 +172,62 @@ class WokRoot(Root): params = parse_request() username = params['username'] password = params['password'] +
It would be good to use strip() around username and password to avoid multiple white spaces.
+ # no data passed: raise error + if len(username + password) == 0: + raise KeyError +
What is this for? You are summing 'username' and 'password' and get the length of it. I am not sure is that what you want.
If it is to identify a missing parameter you should check them individually. As username AND password should be a string with length != 0
Well,
when parse_request run, it never get a missing parameters, so i need to check if the var is empty. I will check each one alone.
Hrm... could you point me the code on where it is done?
From what I remember, the request parameters validation is done through API.json on which each parameter is specified with a required flag. I don't remember we have it for login request. Do we have? Otherwise, there is no way to control knows which parameters are required or not.
Maybe it is time to add a entry for login on API.json to make username and password required fields and non-empty ones. And then you make sure to have only valid values when the request gets to backend (model) code.
except KeyError, item: details = e = MissingParameter('WOKAUTH0003E', {'item': str(item)}) 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)
else: # remove the entry from the dict
ok.
+ 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: - status = e.status - raise + + # 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) +
+ details = e = MissingParameter('WOKAUTH0006E') + raise cherrypy.HTTPError(401, e.message) finally: log_request(code, params, details, method, status)
diff --git a/ui/js/src/wok.login.js b/ui/js/src/wok.login.js index fa2a98a..d9daacf 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 * @@ -19,6 +19,10 @@ */ wok.login_main = function() { "use strict"; + var i18n; + wok.getI18n(function(i18nObj){ + i18n = i18nObj; + }, false, "i18n.json", true);
// verify if language is available var selectedLanguage = wok.lang.get(); @@ -50,7 +54,8 @@ wok.login_main = function() { var query = window.location.search; var error = /.*error=(.*?)(&|$)/g.exec(query); if (error && error[1] === "sessionTimeout") { - $("#messSession").show(); + $("#errorArea").html(i18n["WOKAUT0001E"]); + $("#errorArea").show(); }
var userNameBox = $('#username'); @@ -82,13 +87,13 @@ wok.login_main = function() { window.location.replace(window.location.pathname.replace(/\/+login.html/, '') + next_url); }, function(jqXHR, textStatus, errorThrown) { if (jqXHR.responseText == "") { - $("#messUserPass").hide(); - $("#missServer").show(); - } else { - $("#missServer").hide(); - $("#messUserPass").show(); + $("#errorArea").html(i18n["WOKAUT0002E"]); + $("#errorArea").show(); + } else if ((jqXHR.responseJSON != undefined) && + ! (jqXHR.responseJSON["reason"] == undefined)) { + $("#errorArea").html(jqXHR.responseJSON["reason"]); + $("#errorArea").show(); } - $("#messSession").hide(); $("#logging").hide(); $("#login").show(); }); diff --git a/ui/pages/i18n.json.tmpl b/ui/pages/i18n.json.tmpl index ba29532..4329ad0 100644 --- a/ui/pages/i18n.json.tmpl +++ b/ui/pages/i18n.json.tmpl @@ -1,7 +1,7 @@ #* * Project Wok * - * Copyright IBM Corp, 2014-2016 + * Copyright IBM Corp, 2014-2017 * * Code derived from Project Kimchi * @@ -39,6 +39,9 @@
"WOKHOST6001M": "$_("Max:")",
+ "WOKAUT0001E": "$_("Session timeout, please re-login.")", + "WOKAUT0002E": "$_("Server unreachable")", + "WOKSETT0001M": "$_("Application")", "WOKSETT0002M": "$_("User")", "WOKSETT0003M": "$_("Request")", diff --git a/ui/pages/login.html.tmpl b/ui/pages/login.html.tmpl index f5a4b2d..6f967cf 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 * @@ -104,9 +104,7 @@ <div class="container"> <div id="login-window" class="login-area row"> <div class="err-area"> - <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="errorArea" class="alert alert-danger" style="display: none;"></div> </div> <form id="form-login" class="form-horizontal" method="post"> <div class="form-group">
participants (3)
-
Aline Manera
-
Ramon Medeiros
-
ramonn@linux.vnet.ibm.com