[Kimchi-devel] [PATCH v3] [Wok] Bug fix #147: Block authentication request after too many failures
Aline Manera
alinefm at linux.vnet.ibm.com
Thu Jan 26 11:04:16 UTC 2017
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 at linux.vnet.ibm.com wrote:
>>> From: Ramon Medeiros <ramonn at 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 at 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">
>>
>
More information about the Kimchi-devel
mailing list