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>
---
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