Start learning free

LESSON 20 OF 20 · SECURITY: INTERVIEW QUESTIONS

Does This Email Have an Account?

Practice

An interviewer shows you this piece of a shop's login and asks: what is wrong with it?

user = db.execute("SELECT * FROM users WHERE email = ?", (data["email"],)).fetchone()
if user is None:
    return jsonify({"error": "No such user"}), 404
if not check_password_hash(user["password_hash"], data["password"]):
    return jsonify({"error": "Wrong password"}), 401

Both checks are right, but the two failures answer differently. An attacker tries a login with any password for a long list of emails: every "Wrong password" means "this email has an account here". This is called user enumeration, and the attacker gets a list of your customers, ready for phishing emails and password guessing. A signup that answers "Email taken" leaks the same list.

The fix is one answer for every failure: the same message and the same status, so nobody can tell the cases apart. A signup for a taken email can't create a second account, so it answers exactly like a new signup and tells the truth only by email, to the inbox's owner. Good "forgot password" forms already work this way: they always say "if that email has an account, we sent you a link".

The shop has two customers, ana@example.com and ben@example.com. Your code must give these answers, and send Ben the email "You already have an account" and Zoe "Welcome to the shop":

POST /login   {"email": "ana@example.com", "password": "ana-pass-123"}  →  200  {"email": "ana@example.com", "message": "Logged in"}
POST /login   {"email": "ana@example.com", "password": "guess-123"}     →  401  {"error": "Wrong email or password"}
POST /login   {"email": "zoe@example.com", "password": "guess-123"}     →  401  {"error": "Wrong email or password"}
POST /signup  {"email": "ben@example.com", "password": "guess-123"}     →  202  {"message": "Check your inbox to finish signing up"}
POST /signup  {"email": "zoe@example.com", "password": "zoe-pass-789"}  →  202  {"message": "Check your inbox to finish signing up"}
POST /login   {"email": "ben@example.com", "password": "ben-pass-456"}  →  200  {"email": "ben@example.com", "message": "Logged in"}
POST /login   {"email": "zoe@example.com", "password": "zoe-pass-789"}  →  200  {"email": "zoe@example.com", "message": "Logged in"}

In the editor, both checks are missing. Right now any password logs Ana in, an unknown email crashes the login with a 500, and a taken email crashes the signup with a 500, because the database refuses a second row with the same email. A 500 for one email and a 200 or 202 for another gives the list away just the same.

Your task: in login, answer the same 401 with "Wrong email or password" for an unknown email and for a wrong password. In signup, when the email already has an account, mail that address "You already have an account" and answer the same 202 as a new signup, without touching the account.


Assignment
Follow the Coding Tutorial to give login one answer for both failures, and signup the same answer for a taken email as for a new one.


Hint
One if with or covers both login failures, so they can't answer differently. In signup, return inside the taken-email if, before the INSERT: Ben's password then stays as it was.


STEP 1 OF 5
    Ctrl + Enter to check