mirror of https://github.com/JohnHammond/CTFd.git
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 buttonselenium-screenshot-testing
parent
90b4ee03ad
commit
18d6fa26d2
39
CTFd/auth.py
39
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()
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
Need to resend the confirmation email?
|
||||
</h4>
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" id="submit" tabindex="5" class="btn btn-md btn-primary btn-outlined float-right">Resend</button>
|
||||
<button type="submit" id="submit" tabindex="5" class="btn btn-md btn-primary btn-outlined">Resend</button>
|
||||
</div>
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
</form>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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<script>alert(1)<script>hn3048wueorighkgnsfg%3D%3D'
|
||||
r = client.get(confirm_link)
|
||||
|
||||
assert "Your confirmation token is invalid" 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)
|
||||
|
||||
|
||||
@freeze_time("2019-02-24 03:21:34")
|
||||
def test_expired_reset_password_link():
|
||||
"""Test that expired reset password links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config('mail_server', 'localhost')
|
||||
set_config('mail_port', 25)
|
||||
set_config('mail_username', 'username')
|
||||
set_config('mail_password', 'password')
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
forgot_link = 'http://localhost/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdwa3F5V3hR'
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert "Your link has expired" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_reset_password_link():
|
||||
"""Test that invalid reset password links are reported to the user"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
set_config('mail_server', 'localhost')
|
||||
set_config('mail_port', 25)
|
||||
set_config('mail_username', 'username')
|
||||
set_config('mail_password', 'password')
|
||||
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
# user@user.com "2012-01-14 03:21:34"
|
||||
forgot_link = 'http://localhost/reset_password/5678ytfghjiu876tyfg<>hvbnmkoi9u87y6trdfcgvhbnm,lp09iujmk%3D'
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert "Your reset token is invalid" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_contact_for_password_reset():
|
||||
"""Test that if there is no mailserver configured, users should contact admins"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
register_user(app, name="user1", email="user@user.com")
|
||||
|
||||
with app.test_client() as client:
|
||||
forgot_link = 'http://localhost/reset_password'
|
||||
r = client.get(forgot_link)
|
||||
|
||||
assert "Contact a CTF organizer" in r.get_data(as_text=True)
|
||||
destroy_ctfd(app)
|
||||
|
|
|
@ -580,7 +580,7 @@ def test_user_can_confirm_email(mock_smtp):
|
|||
@patch('smtplib.SMTP')
|
||||
@freeze_time("2012-01-14 03:21:34")
|
||||
def test_user_can_reset_password(mock_smtp):
|
||||
'''Test that a user is capable of resetting their password'''
|
||||
"""Test that a user is capable of resetting their password"""
|
||||
from email.mime.text import MIMEText
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
|
@ -635,7 +635,7 @@ http://localhost/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdw
|
|||
}
|
||||
|
||||
# Do the password reset
|
||||
r = client.get('/reset_password')
|
||||
r = client.get('/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdwa3F5V3hR')
|
||||
r = client.post('/reset_password/InVzZXIxIi5BZktHUGcuTVhkTmZtOWU2U2xwSXZ1MlFwTjdwa3F5V3hR', data=data)
|
||||
|
||||
# Make sure that the user's password changed
|
||||
|
|
Loading…
Reference in New Issue