From 94dc16a0bb4646f96d5c32d2f42199e6b1bc74f7 Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Sat, 14 Mar 2020 19:35:31 +0100 Subject: [PATCH 1/3] Optimize Docker build --- src/Dockerfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Dockerfile b/src/Dockerfile index 19facd4..dcdba7e 100755 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -4,10 +4,11 @@ FROM tiangolo/uwsgi-nginx:python3.7 ENV LISTEN_PORT 8080 EXPOSE 8080 -# Define environment -COPY ./app /app -RUN echo ${groupid} > /app/models/.env WORKDIR /app + +# The requirements file is copied first, so if there are no changes +# docker will skip installing them again, and use a cached image instead +COPY ./app/requirements.txt /app/ ENV PYTHONPATH=/app # Install python dependencies @@ -23,6 +24,10 @@ COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] +# Copy the rest of the project files and define the environment +COPY ./app /app +RUN echo ${groupid} > /app/models/.env + # Allow waiting script to be executed RUN chmod +x ./wait-for-it.sh From 9892487c443b3b886525cf70daf513be8bf9371b Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Sun, 15 Mar 2020 19:57:27 +0100 Subject: [PATCH 2/3] Implement better password security The new scheme uses bcrypt and a random salt for each user. This is not compatible with old passwords. Fixes #13 --- mysql/sql/init.sql | 4 +-- src/app/models/user.py | 56 +++++++++++++++++++-------------------- src/app/requirements.txt | 1 + src/app/views/login.py | 8 +++--- src/app/views/register.py | 11 ++++---- 5 files changed, 39 insertions(+), 41 deletions(-) diff --git a/mysql/sql/init.sql b/mysql/sql/init.sql index 165cbe6..a675d58 100755 --- a/mysql/sql/init.sql +++ b/mysql/sql/init.sql @@ -2,7 +2,7 @@ CREATE TABLE users ( userid INT UNSIGNED AUTO_INCREMENT, username VARCHAR(45) UNIQUE NOT NULL, - password VARCHAR(45) NOT NULL, + password VARCHAR(60) NOT NULL, full_name VARCHAR(200) NOT NULL, company VARCHAR(50), email VARCHAR(50) NOT NULL, @@ -84,7 +84,7 @@ CREATE TABLE task_files ( * Initial data */ -insert into users values (NULL, "admin", "48bead1bb864138c2cafaf1bd41332ab", "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"); insert into project_category values (NULL, "Gardening"); insert into project_category values (NULL, "Programming"); diff --git a/src/app/models/user.py b/src/app/models/user.py index 2b5e9b4..4519115 100755 --- a/src/app/models/user.py +++ b/src/app/models/user.py @@ -2,6 +2,33 @@ from models.database import db import mysql.connector +def get_user(username): + """ + Get the user with the given username + + :param username: The username + :type username: str + :return: user + """ + db.connect() + cursor = db.cursor() + query = ("SELECT userid, username, password from users where username = %s") + user = None + try: + cursor.execute(query, (username,)) + users = cursor.fetchall() + if len(users): + user = users[0] + except mysql.connector.Error as err: + print("Failed executing query: {}".format(err)) + cursor.fetchall() + exit(1) + finally: + cursor.close() + db.close() + return user + + def get_users(): """ Retreive all registrered users from the database @@ -73,32 +100,3 @@ def get_user_name_by_id(userid): cursor.close() db.close() return username - - -def match_user(username, password): - """ - Check if user credentials are correct, return if exists - - :param username: The user attempting to authenticate - :param password: The corresponding password - :type username: str - :type password: str - :return: user - """ - db.connect() - cursor = db.cursor() - query = ("SELECT userid, username from users where username = %s and password = %s") - user = None - try: - cursor.execute(query, (username, password)) - users = cursor.fetchall() - if len(users): - user = users[0] - except mysql.connector.Error as err: - print("Failed executing query: {}".format(err)) - cursor.fetchall() - exit(1) - finally: - cursor.close() - db.close() - return user diff --git a/src/app/requirements.txt b/src/app/requirements.txt index 19df7de..172cf6d 100755 --- a/src/app/requirements.txt +++ b/src/app/requirements.txt @@ -1,3 +1,4 @@ web.py==0.40 mysql-connector-python==8.0.* python-dotenv +bcrypt diff --git a/src/app/views/login.py b/src/app/views/login.py index 58b52c5..99f6d94 100755 --- a/src/app/views/login.py +++ b/src/app/views/login.py @@ -5,7 +5,7 @@ import models.session import models.user import random import string -import hashlib +import bcrypt import time # Get html templates @@ -45,11 +45,9 @@ class Login(): data = web.input(username="", password="", remember=False) # Validate login credential with database query - password_hash = hashlib.md5(b'TDT4237' + data.password.encode('utf-8')).hexdigest() - user = models.user.match_user(data.username, password_hash) + user = models.user.get_user(data.username) - # If there is a matching user/password in the database the user is logged in - if user: + if bcrypt.checkpw(data.password.encode('UTF-8'), user[2].encode('UTF-8')): self.login(user[1], user[0], data.remember) raise web.seeother("/") else: diff --git a/src/app/views/register.py b/src/app/views/register.py index cd79330..ecd414f 100755 --- a/src/app/views/register.py +++ b/src/app/views/register.py @@ -3,7 +3,7 @@ from views.forms import register_form from views.utils import get_nav_bar, csrf_protected import models.register import models.user -import hashlib +import bcrypt import re # Get html templates @@ -41,9 +41,10 @@ class Register: if models.user.get_user_id_by_name(data.username): return render.register(nav, register, "Invalid user, already exists.") - models.register.set_user(data.username, - hashlib.md5(b'TDT4237' + data.password.encode('utf-8')).hexdigest(), - data.full_name, data.company, data.email, data.street_address, - data.city, data.state, data.postal_code, data.country) + password_hash = bcrypt.hashpw(data.password.encode('UTF-8'), bcrypt.gensalt()) + + models.register.set_user(data.username, password_hash, data.full_name, data.company, + data.email, data.street_address, data.city, data.state, + data.postal_code, data.country) return render.register(nav, register_form, "User registered!") From 9491cfd5ddac9e4ef712d89ad10bf251f01aed66 Mon Sep 17 00:00:00 2001 From: Sindre Stephansen Date: Sun, 15 Mar 2020 21:12:52 +0100 Subject: [PATCH 3/3] Implement stricter password policy Fixes #22 --- src/app/views/register.py | 7 ++++- src/app/views/utils.py | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/app/views/register.py b/src/app/views/register.py index ecd414f..2853052 100755 --- a/src/app/views/register.py +++ b/src/app/views/register.py @@ -1,6 +1,6 @@ import web from views.forms import register_form -from views.utils import get_nav_bar, csrf_protected +from views.utils import get_nav_bar, csrf_protected, password_weakness import models.register import models.user import bcrypt @@ -41,6 +41,11 @@ class Register: if models.user.get_user_id_by_name(data.username): return render.register(nav, register, "Invalid user, already exists.") + # Check password security + weakness = password_weakness(data.password, data.username) + if weakness is not None: + return render.register(nav, register, weakness) + password_hash = bcrypt.hashpw(data.password.encode('UTF-8'), bcrypt.gensalt()) models.register.set_user(data.username, password_hash, data.full_name, data.company, diff --git a/src/app/views/utils.py b/src/app/views/utils.py index 8b8a6d3..9b0650b 100755 --- a/src/app/views/utils.py +++ b/src/app/views/utils.py @@ -78,3 +78,61 @@ def csrf_protected(f): return f(*args, **kwargs) return decorated + + +def is_common_password(password): + """Helper function that checks various common passwords.""" + def common_sequences(n): + # Check sequences of the same number + for i in range(n): + for j in range(n): + yield ''.join([str(i) for _ in range(j)]) + + # Check incrementing sequences + for i in range(n): + # Starting at 0 + seq = ''.join([str(j) for j in range(i)]) + yield seq + # Starting at 1 + yield seq[1:] + + # Decrementing + # Starting at 0 + yield seq[::-1] + # Starting at 1 + yield seq[1::-1] + + common_passwords = [ + 'password', 'qwerty', 'iloveyou', '123123', 'abc123', 'admin', + 'passwrod', 'password1', 'beelance', 'beelance2' + ] + + if password in common_passwords or password in common_sequences(12): + return True + + return False + + +def password_weakness(password, username): + """ + Check if the password fulfills the password policy. + + The policy is: + - At least 8 characters, but not more than 70 (due to bcrypt) + - Does not overlap with the username + - Not a common password + + :param password: The password to check + :param username: The username of the user (used to check similarity) + :return: The most important weakness of the password, or None if it fulfills the policy + """ + if len(password) < 8: + return "The password must be at least 5 characters long." + elif len(password) > 70: + return "The password can't be longer than 70 characters." + elif password in username or username in password: + return "The password can't overlap with your username." + elif is_common_password(password): + return "The password is too common. Choose something more unique." + + return None