From 18d6fa26d244c720d4b6e85062f6ac8ca73a703a Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Wed, 13 Dec 2017 22:34:53 -0500 Subject: [PATCH] Adding an error message for invalid confirm/reset links (#525) * Adding an error message for invalid confirm/reset links * Add tests for expired/invalid links * Avoid non-unicode crash in base64 utilities * Centering confirm email button --- CTFd/auth.py | 39 +++++----- CTFd/themes/core/templates/confirm.html | 2 +- CTFd/utils.py | 10 ++- tests/user/test_auth.py | 94 +++++++++++++++++++++++++ tests/user/test_user_facing.py | 4 +- 5 files changed, 127 insertions(+), 22 deletions(-) diff --git a/CTFd/auth.py b/CTFd/auth.py index d3e708e3..e797b42a 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -11,6 +11,8 @@ from CTFd.models import db, Teams from CTFd import utils from CTFd.utils import ratelimit +import base64 + auth = Blueprint('auth', __name__) @@ -30,8 +32,8 @@ def confirm_user(data=None): email = s.loads(utils.base64decode(data, urldecode=True), max_age=1800) except BadTimeSignature: return render_template('confirm.html', errors=['Your confirmation link has expired']) - except BadSignature: - return render_template('confirm.html', errors=['Your confirmation link seems wrong']) + except (BadSignature, TypeError, base64.binascii.Error): + return render_template('confirm.html', errors=['Your confirmation token is invalid']) team = Teams.query.filter_by(email=email).first_or_404() team.verified = True db.session.commit() @@ -80,26 +82,29 @@ def confirm_user(data=None): @ratelimit(method="POST", limit=10, interval=60) def reset_password(data=None): logger = logging.getLogger('logins') - if data is not None and request.method == "GET": - return render_template('reset_password.html', mode='set') - if data is not None and request.method == "POST": + + if data is not None: try: s = TimedSerializer(app.config['SECRET_KEY']) name = s.loads(utils.base64decode(data, urldecode=True), max_age=1800) except BadTimeSignature: return render_template('reset_password.html', errors=['Your link has expired']) - except: - return render_template('reset_password.html', errors=['Your link appears broken, please try again']) - team = Teams.query.filter_by(name=name).first_or_404() - team.password = bcrypt_sha256.encrypt(request.form['password'].strip()) - db.session.commit() - logger.warn("[{date}] {ip} - successful password reset for {username}".format( - date=time.strftime("%m/%d/%Y %X"), - ip=utils.get_ip(), - username=team.name.encode('utf-8') - )) - db.session.close() - return redirect(url_for('auth.login')) + except (BadSignature, TypeError, base64.binascii.Error): + return render_template('reset_password.html', errors=['Your reset token is invalid']) + + if request.method == "GET": + return render_template('reset_password.html', mode='set') + if request.method == "POST": + team = Teams.query.filter_by(name=name).first_or_404() + team.password = bcrypt_sha256.encrypt(request.form['password'].strip()) + db.session.commit() + logger.warn("[{date}] {ip} - successful password reset for {username}".format( + date=time.strftime("%m/%d/%Y %X"), + ip=utils.get_ip(), + username=team.name.encode('utf-8') + )) + db.session.close() + return redirect(url_for('auth.login')) if request.method == 'POST': email = request.form['email'].strip() diff --git a/CTFd/themes/core/templates/confirm.html b/CTFd/themes/core/templates/confirm.html index 7cce0962..1a2ccc29 100644 --- a/CTFd/themes/core/templates/confirm.html +++ b/CTFd/themes/core/templates/confirm.html @@ -46,7 +46,7 @@ Need to resend the confirmation email?
- +
diff --git a/CTFd/utils.py b/CTFd/utils.py index d6470e16..d000e5ab 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -699,7 +699,10 @@ def base64encode(s, urlencode=False): encoded = base64.urlsafe_b64encode(s) if six.PY3: - encoded = encoded.decode('utf-8') + try: + encoded = encoded.decode('utf-8') + except UnicodeDecodeError: + pass if urlencode: encoded = quote(encoded) return encoded @@ -717,7 +720,10 @@ def base64decode(s, urldecode=False): decoded = base64.urlsafe_b64decode(s) if six.PY3: - decoded = decoded.decode('utf-8') + try: + decoded = decoded.decode('utf-8') + except UnicodeDecodeError: + pass return decoded diff --git a/tests/user/test_auth.py b/tests/user/test_auth.py index 1db47623..cf08a6c2 100644 --- a/tests/user/test_auth.py +++ b/tests/user/test_auth.py @@ -103,3 +103,97 @@ def test_verify_and_view_unregistered(): r = client.get('/chals') assert r.status_code == 200 destroy_ctfd(app) + + +@freeze_time("2019-02-24 03:21:34") +def test_expired_confirmation_links(): + """Test that expired confirmation links are reported to the user""" + app = create_ctfd() + with app.app_context(): + set_config('verify_emails', True) + + register_user(app, email="user@user.com") + client = login_as_user(app, name="user", password="password") + + # user@user.com "2012-01-14 03:21:34" + confirm_link = 'http://localhost/confirm/InVzZXJAdXNlci5jb20iLkFmS0dQZy5kLUJnVkgwaUhadzFHaXVENHczWTJCVVJwdWc%3D' + r = client.get(confirm_link) + + assert "Your confirmation link has expired" in r.get_data(as_text=True) + team = Teams.query.filter_by(email='user@user.com').first() + assert team.verified is not True + destroy_ctfd(app) + + +def test_invalid_confirmation_links(): + """Test that invalid confirmation links are reported to the user""" + app = create_ctfd() + with app.app_context(): + set_config('verify_emails', True) + + register_user(app, email="user@user.com") + client = login_as_user(app, name="user", password="password") + + # user@user.com "2012-01-14 03:21:34" + confirm_link = 'http://localhost/confirm/a8375iyu