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(a)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">