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.
Free plan: 30 tutor messages. PRO is unlimited.
