Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Securely send passwords from login form in Flask

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
like image 472
Jack P Avatar asked Nov 21 '25 11:11

Jack P


1 Answers

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='
>>>
like image 159
thebjorn Avatar answered Nov 23 '25 02:11

thebjorn



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!