I have a website with a simple login function using sessions. I thought it was secure because I was hashing and salting the passwords, but I have realised that what's actually (probably) happening is that I am sending the plaintext password in a POST request to the server, and only then am I hashing and salting it. I thought I understood how to manage passwords, but now I'm wondering what to do.
Here's my code:
import bcrypt
from argon2 import PasswordHasher, exceptions
import db as database
ph = PasswordHasher()
@app.route("/login", methods=["POST", "GET"])
def login():
if check_logged_in(session, 0):
return redirect("/index")
else:
error = "" # set to nothing unless we get error below
if request.method == "POST": # if user submits form
username = request.form["username"] # retrieve username from form
password = request.form["password"] # retrieve password from form
if verify_login(username, password): # if their login is valid
session["username"] = username # update session with their username and redirect them to index
return redirect("/index")
error = "Invalid username or password" # else update the error message
return render_template("login.html", error=error) # and return login with error message
def verify_login(username, password):
db_hash, salt = database.retrieve_pw_salt(username) # gets hashed/salted password and salt from database given a username
if db_hash is None:
return False # invalid username
try:
ph.verify(db_hash, salt + password)
return True # valid username and password
except exceptions.VerifyMismatchError:
return False # invalid password
def check_logged_in(session, required_privilege):
if "username" not in session:
return False # not logged in
elif session["username"] == "admin":
return True # logged in as admin
elif session["username"] == "reception" and required_privilege <= 1: # checks to ensure a reception isn't trying to access admin pages/features
return True # logged in as reception
elif session["username"] == "teacher" and required_privilege == 0: # checks to ensure a teacher isn't trying to access admin/reception pages/features
return True # logged in as teacher
return False # else not logged in
I guess I thought that this code would be running on the client side but I don't think it is. In that case, how can I get the salt for my user onto the client side and then combine that with the password, hash it and only then send it to my server?
My server is running Nginx with uWSGI on Ubuntu 18.04 if that makes a difference.
Thanks.
EDIT: This is how I generated the salts in the first place by the way:
def generate_salt(): # creates a salt
salt = bcrypt.gensalt().decode("utf-8")
return salt
I am using HTTPS but I thought this was used as well.
All security measures need to have a clear concept of what they will protect you from, and what you need to do depends on which attack vectors you're trying to cover.
Your concern seems to be that someone can read the password in-transit:
but I have realised that what's actually (probably) happening is that I am sending the plaintext password in a POST request to the server
You are incorrect though, when you say that the password is sent as plain text when you're using https. Https means that the channel between the server and the browser is encrypted, and.. considered safe(*). Encrypting twice doesn't add any security even though it may seem like doing more work should give you "something".
If you're trying to protect against an attacker that has control over the browser or client machine, then no amount of client side encryption will help; the situation is the same if you're trying to protect against an attacker that has control over the server.
One situation that can leak passwords is unhandled exceptions during the POST. Most web-apps report such errors either through email or services such as Sentry. Web-frameworks usually have a way of mitigating this that are more general, and easier to work with, than client side encryption (e.g. Django's @sensitive_post_parameters(...) https://docs.djangoproject.com/en/2.1/howto/error-reporting/#django.views.decorators.debug.sensitive_post_parameters).
One situation where you would use client side encryption is if the user wants to store/retrieve data on the server that the server-admin shouldn't be able to read. The server can't interact with the data, so it's not a use case for passwords during login.
(*) if you want to protect against someone that can decrypt an https channel then you're way outside what's needed for login-form for normal websites (and if you're working for the NSA you should probably ask internally rather than on SO ;-)
ps: you probably shouldn't decode('utf-8') the result of bcrypt.gensalt() -- I haven't looked, but I can't imagine it returns utf-8 data.
addendum: not all binary data is utf-8. base64 is one way to create a unicode representation:
>>> b = b'\xc3\x28' # byte sequence that is not valid utf-8
>>> b.decode('utf-8')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 in position 0: invalid continuation byte
>>> import base64
>>> base64.b64encode(b)
b'wyg='
>>> base64.b64encode(b).decode('ascii')
'wyg='
>>>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With