diff --git a/CTFd/auth.py b/CTFd/auth.py index ab9aeff5..97017aaa 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -2,7 +2,6 @@ import logging import os import re import time -import urllib from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature @@ -25,8 +24,10 @@ def confirm_user(data=None): # User is confirming email account if data and request.method == "GET": try: - s = Signer(app.config['SECRET_KEY']) - email = s.unsign(urllib.unquote_plus(data.decode('base64'))) + s = TimedSerializer(app.config['SECRET_KEY']) + 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: @@ -82,7 +83,7 @@ def reset_password(data=None): if data is not None and request.method == "POST": try: s = TimedSerializer(app.config['SECRET_KEY']) - name = s.loads(urllib.unquote_plus(data.decode('base64')), max_age=1800) + 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: @@ -110,7 +111,7 @@ Did you initiate a password reset? {0}/{1} -""".format(url_for('auth.reset_password', _external=True), urllib.quote_plus(token.encode('base64'))) +""".format(url_for('auth.reset_password', _external=True), utils.base64encode(token, urlencode=True)) utils.sendmail(email, text) diff --git a/CTFd/config.py b/CTFd/config.py index 7a7591eb..64c74c23 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -119,7 +119,9 @@ class Config(object): class TestingConfig(Config): + SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA' PRESERVE_CONTEXT_ON_EXCEPTION = False TESTING = True DEBUG = True SQLALCHEMY_DATABASE_URI = 'sqlite://' + SERVER_NAME = 'localhost' diff --git a/CTFd/utils.py b/CTFd/utils.py index 2bdaeec0..2c3860ab 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -1,3 +1,4 @@ +import base64 import datetime import functools import hashlib @@ -16,7 +17,6 @@ import subprocess import sys import tempfile import time -import urllib import dataset import zipfile import io @@ -25,8 +25,8 @@ from email.mime.text import MIMEText from flask import current_app as app, request, redirect, url_for, session, render_template, abort from flask_caching import Cache from flask_migrate import Migrate, upgrade as migrate_upgrade, stamp as migrate_stamp -from itsdangerous import Signer -from six.moves.urllib.parse import urlparse, urljoin +from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature +from six.moves.urllib.parse import urlparse, urljoin, quote, unquote from werkzeug.utils import secure_filename from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Containers, ip2long, long2ip @@ -495,11 +495,12 @@ def sendmail(addr, text): def verify_email(addr): - s = Signer(app.config['SECRET_KEY']) - token = s.sign(addr) - text = """Please click the following link to confirm your email address for {}: {}""".format( - get_config('ctf_name'), - url_for('auth.confirm_user', _external=True) + '/' + urllib.quote_plus(token.encode('base64')) + s = TimedSerializer(app.config['SECRET_KEY']) + token = s.dumps(addr) + text = """Please click the following link to confirm your email address for {ctf_name}: {url}/{token}""".format( + ctf_name=get_config('ctf_name'), + url=url_for('auth.confirm_user', _external=True), + token=base64encode(token, urlencode=True) ) sendmail(addr, text) @@ -522,6 +523,28 @@ def sha512(string): return hashlib.sha512(string).hexdigest() +def base64encode(s, urlencode=False): + if six.PY3 and isinstance(s, six.string_types): + s = s.encode('utf-8') + encoded = base64.urlsafe_b64encode(s) + if six.PY3: + encoded = encoded.decode('utf-8') + if urlencode: + encoded = quote(encoded) + return encoded + + +def base64decode(s, urldecode=False): + if urldecode: + s = unquote(s) + if six.PY3 and isinstance(s, six.string_types): + s = s.encode('utf-8') + decoded = base64.urlsafe_b64decode(s) + if six.PY3: + decoded = decoded.decode('utf-8') + return decoded + + @cache.memoize() def can_create_container(): try: diff --git a/tests/test_utils.py b/tests/test_utils.py index f09b1ce8..16bbac12 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,8 @@ from tests.helpers import * from CTFd.models import ip2long, long2ip -from CTFd.utils import get_config, set_config, override_template, sendmail +from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email +from CTFd.utils import base64encode, base64decode from mock import patch import json @@ -41,6 +42,20 @@ def test_long2ip_ipv6(): assert long2ip(42540616829182469433547762482097946625) == '2001:658:22a:cafe:200::1' +def test_base64encode(): + """The base64encode wrapper works properly""" + assert base64encode('abc123') == 'YWJjMTIz' + assert base64encode('😆') == '8J-Yhg==' + assert base64encode('😆', urlencode=True) == '8J-Yhg%3D%3D' + + +def test_base64decode(): + """The base64decode wrapper works properly""" + assert base64decode('YWJjMTIz') == 'abc123' + assert base64decode('8J-Yhg==') == '😆' + assert base64decode('8J-Yhg%3D%3D', urldecode=True) == '😆' + + def test_override_template(): """Does override_template work properly for regular themes""" app = create_ctfd() @@ -93,3 +108,37 @@ def test_sendmail_with_smtp(mock_smtp): mock_smtp.return_value.sendmail.assert_called_once_with(from_addr, [to_addr], email_msg.as_string()) destroy_ctfd(app) + + +@patch('smtplib.SMTP') +def test_verify_email(mock_smtp): + """Does verify_email send emails""" + from email.mime.text import MIMEText + 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') + set_config('verify_emails', True) + + from_addr = get_config('mailfrom_addr') or app.config.get('MAILFROM_ADDR') + to_addr = 'user@user.com' + + verify_email(to_addr) + + # This is currently not actually validated + msg = ("Please click the following link to confirm" + " your email address for CTFd:" + " http://localhost/confirm/InVzZXJAdXNlci5jb20iLkRHbGFZUS5XUURfQzBub3pGZkFMRlIyeGxDS1BCMjZETlk%3D") + + ctf_name = get_config('ctf_name') + email_msg = MIMEText(msg) + email_msg['Subject'] = "Message from {0}".format(ctf_name) + email_msg['From'] = from_addr + email_msg['To'] = to_addr + + # Need to freeze time to predict the value of the itsdangerous token. + # For now just assert that sendmail was called. + mock_smtp.return_value.sendmail.assert_called() + destroy_ctfd(app)