From 24bc79c57514054cf2dc45bdb0ca6a384da61791 Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Mon, 16 Mar 2020 18:19:16 +0100 Subject: [PATCH] Implement protection from brute-force attacks The implementation enforces a timeout of one minute after three or more incorrect login attempts for an account. Fixes #8 --- mysql/sql/init.sql | 4 +++- src/app/models/register.py | 2 +- src/app/models/user.py | 21 ++++++++++++++++++++- src/app/views/login.py | 23 ++++++++++++++++++++--- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/mysql/sql/init.sql b/mysql/sql/init.sql index a675d58..8733a71 100755 --- a/mysql/sql/init.sql +++ b/mysql/sql/init.sql @@ -11,6 +11,8 @@ CREATE TABLE users ( state VARCHAR(50), postal_code VARCHAR(50), country VARCHAR(50), + login_attempts INT UNSIGNED, + last_login_attempt INT UNSIGNED, PRIMARY KEY (userid) ); @@ -84,7 +86,7 @@ CREATE TABLE task_files ( * Initial data */ -insert into users values (NULL, "admin", "$2b$12$iKbYZ0MFwWWxoYUXKRhFiOPo7itaQO2DIRnLgXbECsj8XKVzkNCSi", "Admin Modsen", "ntnu", 'mail@ntnu.no', "street", "trondheim", "trondheim", "1234", "norway"); +insert into users values (NULL, "admin", "$2b$12$iKbYZ0MFwWWxoYUXKRhFiOPo7itaQO2DIRnLgXbECsj8XKVzkNCSi", "Admin Modsen", "ntnu", 'mail@ntnu.no', "street", "trondheim", "trondheim", "1234", "norway", 0, 0); insert into project_category values (NULL, "Gardening"); insert into project_category values (NULL, "Programming"); diff --git a/src/app/models/register.py b/src/app/models/register.py index 52f1d22..d416c0b 100755 --- a/src/app/models/register.py +++ b/src/app/models/register.py @@ -28,7 +28,7 @@ def set_user(username, password, full_name, company, email, """ db.connect() cursor = db.cursor() - query = ("INSERT INTO users VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)") + query = ("INSERT INTO users VALUES (NULL, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0, 0)") try: cursor.execute(query, (username, password, full_name, company, email, street_address, city, state, postal_code, country)) diff --git a/src/app/models/user.py b/src/app/models/user.py index 4519115..01a7c98 100755 --- a/src/app/models/user.py +++ b/src/app/models/user.py @@ -12,7 +12,7 @@ def get_user(username): """ db.connect() cursor = db.cursor() - query = ("SELECT userid, username, password from users where username = %s") + query = ("SELECT userid, username, password, login_attempts, last_login_attempt from users where username = %s") user = None try: cursor.execute(query, (username,)) @@ -51,6 +51,25 @@ def get_users(): return users +def set_login_attempts(userid, num, timestamp): + """ + Set the number and timestamp of the failed login attempts for the given user. + """ + db.connect() + cursor = db.cursor() + query = ("UPDATE users SET login_attempts = %s, last_login_attempt = %s WHERE userid = %s") + try: + cursor.execute(query, (num, timestamp, userid)) + db.commit() + except mysql.connector.Error as err: + print("Failed executing query: {}".format(err)) + cursor.fetchall() + exit(1) + finally: + cursor.close() + db.close() + + def get_user_id_by_name(username): """ Get the id of the unique username diff --git a/src/app/views/login.py b/src/app/views/login.py index 99f6d94..1c0df3e 100755 --- a/src/app/views/login.py +++ b/src/app/views/login.py @@ -14,6 +14,10 @@ render = web.template.render('templates/') # The remember cookie should be valid for a week remember_timeout = 3600*24*7 +# The timeout between login attempts, after the 3rd incorrect one +login_timeout = 60 +login_attempts_threshold = 2 + class Login(): @@ -47,11 +51,24 @@ class Login(): # Validate login credential with database query user = models.user.get_user(data.username) - if bcrypt.checkpw(data.password.encode('UTF-8'), user[2].encode('UTF-8')): - self.login(user[1], user[0], data.remember) + if user is None: + return render.login(nav, login_form, "- User authentication failed") + + userid, username, password_hash, login_attempts, last_login_attempt = user + + if login_attempts > login_attempts_threshold and last_login_attempt + login_timeout > time.time(): + return render.login(nav, login_form, "- There have been too many incorrect login attempts for your account. You have to wait a minute before you can log in.") + + if bcrypt.checkpw(data.password.encode('UTF-8'), password_hash.encode('UTF-8')): + models.user.set_login_attempts(userid, 0, time.time()) + self.login(username, userid, data.remember) raise web.seeother("/") else: - return render.login(nav, login_form, "- User authentication failed") + models.user.set_login_attempts(userid, login_attempts+1, time.time()) + if login_attempts == login_attempts_threshold: + return render.login(nav, login_form, "- Too many incorrect login attempts. You have to wait a minute before trying again.") + else: + return render.login(nav, login_form, "- User authentication failed") def login(self, username, userid, remember): """