Strip spaces on registration and have reset password use email address instead of names (#1218)

* Usernames are now properly stripped before being used in registration checks
* Reset password function now uses email addresses instead of user names for tokens
* Prevent MLC users from resetting their password
This commit is contained in:
Kevin Chung
2020-01-20 14:22:06 -05:00
committed by GitHub
parent fe85fdf1e5
commit f660ed1fb7
7 changed files with 71 additions and 33 deletions

View File

@@ -98,7 +98,7 @@ def confirm(data=None):
def reset_password(data=None): def reset_password(data=None):
if data is not None: if data is not None:
try: try:
name = unserialize(data, max_age=1800) email_address = unserialize(data, max_age=1800)
except (BadTimeSignature, SignatureExpired): except (BadTimeSignature, SignatureExpired):
return render_template( return render_template(
"reset_password.html", errors=["Your link has expired"] "reset_password.html", errors=["Your link has expired"]
@@ -111,20 +111,35 @@ def reset_password(data=None):
if request.method == "GET": if request.method == "GET":
return render_template("reset_password.html", mode="set") return render_template("reset_password.html", mode="set")
if request.method == "POST": if request.method == "POST":
user = Users.query.filter_by(name=name).first_or_404() password = request.form.get("password", "").strip()
user.password = request.form["password"].strip() user = Users.query.filter_by(email=email_address).first_or_404()
if user.oauth_id:
return render_template(
"reset_password.html",
errors=[
"Your account was registered via an authentication provider and does not have an associated password. Please login via your authentication provider."
],
)
pass_short = len(password) == 0
if pass_short:
return render_template(
"reset_password.html", errors=["Please pick a longer password"]
)
user.password = password
db.session.commit() db.session.commit()
log( log(
"logins", "logins",
format="[{date}] {ip} - successful password reset for {name}", format="[{date}] {ip} - successful password reset for {name}",
name=name, name=user.name,
) )
db.session.close() db.session.close()
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
if request.method == "POST": if request.method == "POST":
email_address = request.form["email"].strip() email_address = request.form["email"].strip()
team = Users.query.filter_by(email=email_address).first() user = Users.query.filter_by(email=email_address).first()
get_errors() get_errors()
@@ -134,7 +149,7 @@ def reset_password(data=None):
errors=["Email could not be sent due to server misconfiguration"], errors=["Email could not be sent due to server misconfiguration"],
) )
if not team: if not user:
return render_template( return render_template(
"reset_password.html", "reset_password.html",
errors=[ errors=[
@@ -142,7 +157,15 @@ def reset_password(data=None):
], ],
) )
email.forgot_password(email_address, team.name) if user.oauth_id:
return render_template(
"reset_password.html",
errors=[
"The email address associated with this account was registered via an authentication provider and does not have an associated password. Please login via your authentication provider."
],
)
email.forgot_password(email_address)
return render_template( return render_template(
"reset_password.html", "reset_password.html",
@@ -159,9 +182,9 @@ def reset_password(data=None):
def register(): def register():
errors = get_errors() errors = get_errors()
if request.method == "POST": if request.method == "POST":
name = request.form["name"] name = request.form.get("name", "").strip()
email_address = request.form["email"] email_address = request.form.get("email", "").strip().lower()
password = request.form["password"] password = request.form.get("password", "").strip()
name_len = len(name) == 0 name_len = len(name) == 0
names = Users.query.add_columns("name", "id").filter_by(name=name).first() names = Users.query.add_columns("name", "id").filter_by(name=name).first()
@@ -170,9 +193,9 @@ def register():
.filter_by(email=email_address) .filter_by(email=email_address)
.first() .first()
) )
pass_short = len(password.strip()) == 0 pass_short = len(password) == 0
pass_long = len(password) > 128 pass_long = len(password) > 128
valid_email = validators.validate_email(request.form["email"]) valid_email = validators.validate_email(email_address)
team_name_email_check = validators.validate_email(name) team_name_email_check = validators.validate_email(name)
if not valid_email: if not valid_email:
@@ -206,11 +229,7 @@ def register():
) )
else: else:
with app.app_context(): with app.app_context():
user = Users( user = Users(name=name, email=email_address, password=password)
name=name.strip(),
email=email_address.lower(),
password=password.strip(),
)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
db.session.flush() db.session.flush()

View File

@@ -50,6 +50,7 @@ class TeamSchema(ma.ModelSchema):
name = data.get("name") name = data.get("name")
if name is None: if name is None:
return return
name = name.strip()
existing_team = Teams.query.filter_by(name=name).first() existing_team = Teams.query.filter_by(name=name).first()
current_team = get_current_team() current_team = get_current_team()

View File

@@ -55,6 +55,7 @@ class UserSchema(ma.ModelSchema):
name = data.get("name") name = data.get("name")
if name is None: if name is None:
return return
name = name.strip()
existing_user = Users.query.filter_by(name=name).first() existing_user = Users.query.filter_by(name=name).first()
current_user = get_current_user() current_user = get_current_user()
@@ -95,6 +96,7 @@ class UserSchema(ma.ModelSchema):
email = data.get("email") email = data.get("email")
if email is None: if email is None:
return return
email = email.strip()
existing_user = Users.query.filter_by(email=email).first() existing_user = Users.query.filter_by(email=email).first()
current_user = get_current_user() current_user = get_current_user()

View File

@@ -105,7 +105,7 @@ def new():
return render_template("teams/new_team.html", infos=infos, errors=errors) return render_template("teams/new_team.html", infos=infos, errors=errors)
elif request.method == "POST": elif request.method == "POST":
teamname = request.form.get("name") teamname = request.form.get("name", "").strip()
passphrase = request.form.get("password", "").strip() passphrase = request.form.get("password", "").strip()
errors = get_errors() errors = get_errors()

View File

@@ -16,17 +16,17 @@ def sendmail(addr, text, subject="Message from {ctf_name}"):
return False, "No mail settings configured" return False, "No mail settings configured"
def forgot_password(email, team_name): def forgot_password(email):
token = serialize(team_name) token = serialize(email)
text = """Did you initiate a password reset? Click the following link to reset your password: text = """Did you initiate a password reset? If you didn't initiate this request you can ignore this email.
Click the following link to reset your password:
{0}/{1} {0}/{1}
""".format( """.format(
url_for("auth.reset_password", _external=True), token url_for("auth.reset_password", _external=True), token
) )
subject = "Password Reset Request from {ctf_name}"
return sendmail(email, text) return sendmail(addr=email, text=text, subject=subject)
def verify_email_address(addr): def verify_email_address(addr):
@@ -36,7 +36,8 @@ def verify_email_address(addr):
url=url_for("auth.confirm", _external=True), url=url_for("auth.confirm", _external=True),
token=token, token=token,
) )
return sendmail(addr, text) subject = "Confirm your account for {ctf_name}"
return sendmail(addr=addr, text=text, subject=subject)
def user_created_notification(addr, name, password): def user_created_notification(addr, name, password):

View File

@@ -48,6 +48,13 @@ def test_register_duplicate_username():
password="password", password="password",
raise_for_error=False, raise_for_error=False,
) )
register_user(
app,
name="admin ",
email="admin2@ctfd.io",
password="password",
raise_for_error=False,
)
user_count = Users.query.count() user_count = Users.query.count()
assert user_count == 2 # There's the admin user and the first created user assert user_count == 2 # There's the admin user and the first created user
destroy_ctfd(app) destroy_ctfd(app)
@@ -353,11 +360,15 @@ def test_user_can_reset_password(mock_smtp):
# Build the email # Build the email
msg = ( msg = (
"""Did you initiate a password reset? Click the following link to reset """ "Did you initiate a password reset? If you didn't initiate this request you can ignore this email."
"""your password:\n\nhttp://localhost/reset_password/InVzZXIxIg.TxD0vg.-gvVg-KVy0RWkiclAE6JViv1I0M\n\n""" "\n\nClick the following link to reset your password:\n"
"http://localhost/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U\n"
) )
ctf_name = get_config("ctf_name")
email_msg = MIMEText(msg) email_msg = MIMEText(msg)
email_msg["Subject"] = "Message from CTFd" email_msg["Subject"] = "Password Reset Request from {ctf_name}".format(
ctf_name=ctf_name
)
email_msg["From"] = from_addr email_msg["From"] = from_addr
email_msg["To"] = to_addr email_msg["To"] = to_addr
@@ -374,9 +385,11 @@ def test_user_can_reset_password(mock_smtp):
data = {"nonce": sess.get("nonce"), "password": "passwordtwo"} data = {"nonce": sess.get("nonce"), "password": "passwordtwo"}
# Do the password reset # Do the password reset
client.get("/reset_password/InVzZXIxIg.TxD0vg.-gvVg-KVy0RWkiclAE6JViv1I0M") client.get(
"/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U"
)
client.post( client.post(
"/reset_password/InVzZXIxIg.TxD0vg.-gvVg-KVy0RWkiclAE6JViv1I0M", "/reset_password/InVzZXJAdXNlci5jb20i.TxD0vg.28dY_Gzqb1TH9nrcE_H7W8YFM-U",
data=data, data=data,
) )

View File

@@ -156,7 +156,6 @@ def test_sendmail_with_mailgun_from_db_config(fake_post_request):
@patch("smtplib.SMTP") @patch("smtplib.SMTP")
@freeze_time("2012-01-14 03:21:34")
def test_verify_email(mock_smtp): def test_verify_email(mock_smtp):
"""Does verify_email send emails""" """Does verify_email send emails"""
app = create_ctfd() app = create_ctfd()
@@ -171,6 +170,7 @@ def test_verify_email(mock_smtp):
from_addr = get_config("mailfrom_addr") or app.config.get("MAILFROM_ADDR") from_addr = get_config("mailfrom_addr") or app.config.get("MAILFROM_ADDR")
to_addr = "user@user.com" to_addr = "user@user.com"
with freeze_time("2012-01-14 03:21:34"):
verify_email_address(to_addr) verify_email_address(to_addr)
# This is currently not actually validated # This is currently not actually validated
@@ -182,7 +182,9 @@ def test_verify_email(mock_smtp):
ctf_name = get_config("ctf_name") ctf_name = get_config("ctf_name")
email_msg = MIMEText(msg) email_msg = MIMEText(msg)
email_msg["Subject"] = "Message from {0}".format(ctf_name) email_msg["Subject"] = "Confirm your account for {ctf_name}".format(
ctf_name=ctf_name
)
email_msg["From"] = from_addr email_msg["From"] = from_addr
email_msg["To"] = to_addr email_msg["To"] = to_addr