838 lines
29 KiB
Python
838 lines
29 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
from unittest.mock import patch
|
|
|
|
from freezegun import freeze_time
|
|
|
|
from CTFd.models import Users, db
|
|
from CTFd.utils import get_config, set_config
|
|
from CTFd.utils.crypto import verify_password
|
|
from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user
|
|
|
|
|
|
def test_register_user():
|
|
"""Can a user be registered"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
user_count = Users.query.count()
|
|
assert user_count == 2 # There's the admin user and the created user
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_register_unicode_user():
|
|
"""Can a user with a unicode name be registered"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app, name="你好")
|
|
user_count = Users.query.count()
|
|
assert user_count == 2 # There's the admin user and the created user
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_register_duplicate_username():
|
|
"""A user shouldn't be able to use an already registered team name"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(
|
|
app,
|
|
name="user1",
|
|
email="user1@examplectf.com",
|
|
password="password",
|
|
raise_for_error=False,
|
|
)
|
|
register_user(
|
|
app,
|
|
name="user1",
|
|
email="user2@examplectf.com",
|
|
password="password",
|
|
raise_for_error=False,
|
|
)
|
|
register_user(
|
|
app,
|
|
name="admin ",
|
|
email="admin2@examplectf.com",
|
|
password="password",
|
|
raise_for_error=False,
|
|
)
|
|
user_count = Users.query.count()
|
|
assert user_count == 2 # There's the admin user and the first created user
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_register_duplicate_email():
|
|
"""A user shouldn't be able to use an already registered email address"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(
|
|
app,
|
|
name="user1",
|
|
email="user1@examplectf.com",
|
|
password="password",
|
|
raise_for_error=False,
|
|
)
|
|
register_user(
|
|
app,
|
|
name="user2",
|
|
email="user1@examplectf.com",
|
|
password="password",
|
|
raise_for_error=False,
|
|
)
|
|
user_count = Users.query.count()
|
|
assert user_count == 2 # There's the admin user and the first created user
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_register_whitelisted_email():
|
|
"""A user shouldn't be able to register with an email that isn't on the whitelist"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
set_config(
|
|
"domain_whitelist", "whitelisted.com, whitelisted.org, whitelisted.net"
|
|
)
|
|
register_user(
|
|
app, name="not_whitelisted", email="user@nope.com", raise_for_error=False
|
|
)
|
|
assert Users.query.count() == 1
|
|
|
|
register_user(app, name="user1", email="user@whitelisted.com")
|
|
assert Users.query.count() == 2
|
|
|
|
register_user(app, name="user2", email="user@whitelisted.org")
|
|
assert Users.query.count() == 3
|
|
|
|
register_user(app, name="user3", email="user@whitelisted.net")
|
|
assert Users.query.count() == 4
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_register_blacklisted_email():
|
|
"""A user shouldn't be able to register with an email that is on the blacklist"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
set_config(
|
|
"domain_blacklist", "blacklisted.com, blacklisted.org, blacklisted.net"
|
|
)
|
|
register_user(
|
|
app, name="blacklisted", email="user@blacklisted.com", raise_for_error=False
|
|
)
|
|
assert Users.query.count() == 1
|
|
|
|
register_user(app, name="user1", email="user@yep.com")
|
|
assert Users.query.count() == 2
|
|
|
|
register_user(app, name="user2", email="user@yay.org")
|
|
assert Users.query.count() == 3
|
|
|
|
register_user(app, name="user3", email="user@yipee.net")
|
|
assert Users.query.count() == 4
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_user_bad_login():
|
|
"""A user should not be able to login with an incorrect password"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(
|
|
app, name="user", password="wrong_password", raise_for_error=False
|
|
)
|
|
with client.session_transaction() as sess:
|
|
assert sess.get("id") is None
|
|
r = client.get("/profile")
|
|
assert r.location.startswith("/login") # We got redirected to login
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_user_login():
|
|
"""Can a registered user can login"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app)
|
|
r = client.get("/profile")
|
|
assert r.location is None # We didn't get redirected to login
|
|
assert r.status_code == 200
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_user_login_with_email():
|
|
"""Can a registered user can login with an email address instead of a team name"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app, name="user@examplectf.com", password="password")
|
|
r = client.get("/profile")
|
|
assert r.location is None # We didn't get redirected to login
|
|
assert r.status_code == 200
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_user_get_logout():
|
|
"""Can a registered user load /logout"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app)
|
|
client.get("/logout", follow_redirects=True)
|
|
r = client.get("/challenges")
|
|
assert r.location == "/login?next=%2Fchallenges%3F"
|
|
assert r.status_code == 302
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_user_isnt_admin():
|
|
"""A registered user cannot access admin pages"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app)
|
|
for page in [
|
|
"pages",
|
|
"users",
|
|
"teams",
|
|
"scoreboard",
|
|
"challenges",
|
|
"statistics",
|
|
"config",
|
|
]:
|
|
r = client.get("/admin/{}".format(page))
|
|
assert r.location.startswith("/login?next=")
|
|
assert r.status_code == 302
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_expired_confirmation_links():
|
|
"""Test that expired confirmation 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_useauth", True)
|
|
set_config("mail_username", "username")
|
|
set_config("mail_password", "password")
|
|
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/bb8a8526146e50778b211ae63074595880edbc0b"
|
|
)
|
|
r = client.get(confirm_link)
|
|
|
|
assert (
|
|
"Your confirmation link is invalid, please generate a new one"
|
|
in r.get_data(as_text=True)
|
|
)
|
|
user = Users.query.filter_by(email="user@user.com").first()
|
|
assert user.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("mail_server", "localhost")
|
|
set_config("mail_port", 25)
|
|
set_config("mail_useauth", True)
|
|
set_config("mail_username", "username")
|
|
set_config("mail_password", "password")
|
|
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"
|
|
r = client.get(confirm_link)
|
|
|
|
assert (
|
|
"Your confirmation link is invalid, please generate a new one"
|
|
in r.get_data(as_text=True)
|
|
)
|
|
user = Users.query.filter_by(email="user@user.com").first()
|
|
assert user.verified is not True
|
|
destroy_ctfd(app)
|
|
|
|
|
|
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_useauth", True)
|
|
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:
|
|
forgot_link = "http://localhost/reset_password/bb8a8526146e50778b211ae63074595880edbc0b"
|
|
r = client.get(forgot_link)
|
|
|
|
assert (
|
|
"Your reset link is invalid, please generate a new one"
|
|
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_useauth", True)
|
|
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<INVALID DATA>hvbnmkoi9u87y6trdf"
|
|
r = client.get(forgot_link)
|
|
|
|
assert (
|
|
"Your reset link is invalid, please generate a new one"
|
|
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 an organizer" in r.get_data(as_text=True)
|
|
destroy_ctfd(app)
|
|
|
|
|
|
@patch("smtplib.SMTP")
|
|
def test_user_can_confirm_email(mock_smtp):
|
|
"""Test that a user is capable of confirming their email address"""
|
|
app = create_ctfd()
|
|
with app.app_context(), freeze_time("2012-01-14 03:21:34"):
|
|
# Set CTFd to only allow confirmed users and send emails
|
|
set_config("verify_emails", True)
|
|
set_config("mail_server", "localhost")
|
|
set_config("mail_port", 25)
|
|
set_config("mail_useauth", True)
|
|
set_config("mail_username", "username")
|
|
set_config("mail_password", "password")
|
|
|
|
register_user(app, name="user1", email="user@user.com")
|
|
|
|
# Teams are not verified by default
|
|
user = Users.query.filter_by(email="user@user.com").first()
|
|
assert user.verified is False
|
|
|
|
client = login_as_user(app, name="user1", password="password")
|
|
|
|
r = client.get("/confirm")
|
|
assert "We've sent a confirmation email" in r.get_data(as_text=True)
|
|
|
|
# smtp send message function was called
|
|
mock_smtp.return_value.send_message.assert_called()
|
|
|
|
with client.session_transaction() as sess:
|
|
urandom_value = b"\xff" * 32
|
|
with patch("os.urandom", return_value=urandom_value):
|
|
data = {"nonce": sess.get("nonce")}
|
|
r = client.post("http://localhost/confirm", data=data)
|
|
assert "Confirmation email sent to" in r.get_data(as_text=True)
|
|
|
|
r = client.get("/challenges")
|
|
assert r.location == "/confirm" # We got redirected to /confirm
|
|
|
|
r = client.get(
|
|
"http://localhost/confirm/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
|
)
|
|
assert r.location == "/challenges"
|
|
|
|
# The team is now verified
|
|
user = Users.query.filter_by(email="user@user.com").first()
|
|
assert user.verified is True
|
|
|
|
r = client.get("http://localhost/confirm")
|
|
assert r.location == "/settings"
|
|
destroy_ctfd(app)
|
|
|
|
|
|
@patch("smtplib.SMTP")
|
|
def test_user_can_reset_password(mock_smtp):
|
|
"""Test that a user is capable of resetting their password"""
|
|
from email.message import EmailMessage
|
|
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Set CTFd to send emails
|
|
set_config("mail_server", "localhost")
|
|
set_config("mail_port", 25)
|
|
set_config("mail_useauth", True)
|
|
set_config("mail_username", "username")
|
|
set_config("mail_password", "password")
|
|
|
|
# Create a user
|
|
register_user(app, name="user1", email="user@user.com")
|
|
|
|
with app.test_client() as client:
|
|
client.get("/reset_password")
|
|
|
|
# Build reset password data
|
|
with client.session_transaction() as sess:
|
|
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
|
|
|
# Issue the password reset request
|
|
urandom_value = b"\xff" * 32
|
|
with patch("os.urandom", return_value=urandom_value):
|
|
client.post("/reset_password", data=data)
|
|
|
|
ctf_name = get_config("ctf_name")
|
|
from_addr = get_config("mailfrom_addr") or app.config.get("MAILFROM_ADDR")
|
|
from_addr = "{} <{}>".format(ctf_name, from_addr)
|
|
|
|
to_addr = "user@user.com"
|
|
|
|
# Build the email
|
|
msg = (
|
|
"Did you initiate a password reset on CTFd? If you didn't initiate this request you can ignore this email. "
|
|
"\n\nClick the following link to reset your password:\n"
|
|
"http://localhost/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\n"
|
|
"If the link is not clickable, try copying and pasting it into your browser."
|
|
)
|
|
ctf_name = get_config("ctf_name")
|
|
|
|
email_msg = EmailMessage()
|
|
email_msg.set_content(msg)
|
|
|
|
email_msg["Subject"] = "Password Reset Request from {ctf_name}".format(
|
|
ctf_name=ctf_name
|
|
)
|
|
email_msg["From"] = from_addr
|
|
email_msg["To"] = to_addr
|
|
|
|
# Make sure that the reset password email is sent
|
|
mock_smtp.return_value.send_message.assert_called()
|
|
assert str(mock_smtp.return_value.send_message.call_args[0][0]) == str(
|
|
email_msg
|
|
)
|
|
|
|
# Get user's original password
|
|
user = Users.query.filter_by(email="user@user.com").first()
|
|
|
|
# Build the POST data
|
|
with client.session_transaction() as sess:
|
|
data = {"nonce": sess.get("nonce"), "password": "passwordtwo"}
|
|
|
|
# Do the password reset
|
|
client.get(
|
|
"/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
|
)
|
|
client.post(
|
|
"/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
data=data,
|
|
)
|
|
|
|
# Make sure that the user's password changed
|
|
user = Users.query.filter_by(email="user@user.com").first()
|
|
assert verify_password("passwordtwo", user.password)
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_banned_user():
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
register_user(app)
|
|
client = login_as_user(app)
|
|
user = Users.query.filter_by(id=2).first()
|
|
user.banned = True
|
|
db.session.commit()
|
|
|
|
routes = ["/", "/challenges", "/api/v1/challenges"]
|
|
for route in routes:
|
|
r = client.get(route)
|
|
assert r.status_code == 403
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_registration_code_required():
|
|
"""
|
|
Test that registration code configuration properly blocks logins
|
|
with missing and incorrect registration codes
|
|
"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Set a registration code
|
|
set_config("registration_code", "secret-sauce")
|
|
|
|
with app.test_client() as client:
|
|
# Load CSRF nonce
|
|
r = client.get("/register")
|
|
resp = r.get_data(as_text=True)
|
|
assert "Registration Code" in resp
|
|
with client.session_transaction() as sess:
|
|
data = {
|
|
"name": "user",
|
|
"email": "user1@examplectf.com",
|
|
"password": "password",
|
|
"nonce": sess.get("nonce"),
|
|
}
|
|
# Attempt registration without password
|
|
r = client.post("/register", data=data)
|
|
resp = r.get_data(as_text=True)
|
|
assert "The registration code you entered was incorrect" in resp
|
|
|
|
# Attempt registration with wrong password
|
|
data["registration_code"] = "wrong-sauce"
|
|
r = client.post("/register", data=data)
|
|
resp = r.get_data(as_text=True)
|
|
assert "The registration code you entered was incorrect" in resp
|
|
|
|
# Attempt registration with right password
|
|
data["registration_code"] = "secret-sauce"
|
|
r = client.post("/register", data=data)
|
|
assert r.status_code == 302
|
|
assert r.location.startswith("/challenges")
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_registration_code_allows_numeric():
|
|
"""
|
|
Test that registration code is allowed to be all numeric
|
|
"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Set a registration code
|
|
set_config("registration_code", "1234567890")
|
|
|
|
with app.test_client() as client:
|
|
# Load CSRF nonce
|
|
r = client.get("/register")
|
|
resp = r.get_data(as_text=True)
|
|
assert "Registration Code" in resp
|
|
with client.session_transaction() as sess:
|
|
data = {
|
|
"name": "user",
|
|
"email": "user1@examplectf.com",
|
|
"password": "password",
|
|
"nonce": sess.get("nonce"),
|
|
}
|
|
|
|
# Attempt registration with numeric registration code
|
|
data["registration_code"] = "1234567890"
|
|
r = client.post("/register", data=data)
|
|
assert r.status_code == 302
|
|
assert r.location.startswith("/challenges")
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_registration_password_minimum_length():
|
|
"""
|
|
Test that registration enforces minimum password length when configured
|
|
"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Set a minimum password length
|
|
set_config("password_min_length", 8)
|
|
|
|
with app.test_client() as client:
|
|
# Load CSRF nonce
|
|
r = client.get("/register")
|
|
with client.session_transaction() as sess:
|
|
data = {
|
|
"name": "user",
|
|
"email": "user1@examplectf.com",
|
|
"password": "short", # Only 5 characters
|
|
"nonce": sess.get("nonce"),
|
|
}
|
|
|
|
# Attempt registration with password too short
|
|
r = client.post("/register", data=data)
|
|
resp = r.get_data(as_text=True)
|
|
assert "Password must be at least 8 characters" in resp
|
|
assert r.status_code == 200 # Should stay on registration page
|
|
|
|
# Verify user was not created
|
|
user_count = Users.query.count()
|
|
assert user_count == 1 # Only admin user exists
|
|
|
|
# Attempt registration with password meeting minimum length
|
|
data["password"] = "validpassword" # 13 characters, meets minimum
|
|
r = client.post("/register", data=data)
|
|
assert r.status_code == 302
|
|
assert r.location.startswith("/challenges")
|
|
|
|
# Verify user was created
|
|
user_count = Users.query.count()
|
|
assert user_count == 2 # Admin user + new user
|
|
|
|
# Test with minimum length set to 0 (disabled)
|
|
set_config("password_min_length", 0)
|
|
|
|
with app.test_client() as client:
|
|
# Load CSRF nonce
|
|
r = client.get("/register")
|
|
with client.session_transaction() as sess:
|
|
data = {
|
|
"name": "user2",
|
|
"email": "user2@examplectf.com",
|
|
"password": "x", # Only 1 character
|
|
"nonce": sess.get("nonce"),
|
|
}
|
|
|
|
# Should allow short password when minimum length is 0
|
|
r = client.post("/register", data=data)
|
|
assert r.status_code == 302
|
|
assert r.location.startswith("/challenges")
|
|
|
|
# Verify user was created
|
|
user_count = Users.query.count()
|
|
assert user_count == 3 # Admin user + 2 new users
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_user_change_password_required():
|
|
"""
|
|
Test that users with change_password=True are redirected to reset password
|
|
and cannot access other pages until they change their password
|
|
"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Create a user with change_password=True
|
|
register_user(
|
|
app, name="testuser", email="test@example.com", password="oldpassword"
|
|
)
|
|
user = Users.query.filter_by(name="testuser").first()
|
|
user.change_password = True
|
|
db.session.commit()
|
|
|
|
with app.test_client() as client:
|
|
# Login as the user
|
|
with client.session_transaction() as sess:
|
|
data = {
|
|
"name": "testuser",
|
|
"password": "oldpassword",
|
|
"nonce": sess.get("nonce"),
|
|
}
|
|
|
|
# Get login page first to get nonce
|
|
client.get("/login")
|
|
with client.session_transaction() as sess:
|
|
data["nonce"] = sess.get("nonce")
|
|
|
|
# Login
|
|
r = client.post("/login", data=data)
|
|
assert r.status_code == 302
|
|
|
|
# Test that user is redirected to reset_password when accessing various pages
|
|
protected_routes = [
|
|
"/",
|
|
"/challenges",
|
|
"/scoreboard",
|
|
"/profile",
|
|
"/settings",
|
|
"/api/v1/challenges",
|
|
]
|
|
|
|
for route in protected_routes:
|
|
r = client.get(route, follow_redirects=False)
|
|
# Should be redirected to reset_password with a token
|
|
assert r.status_code == 302
|
|
assert "/reset_password/" in r.location
|
|
|
|
# Test that the user can access the reset_password page directly
|
|
r = client.get(route, follow_redirects=True)
|
|
# Should end up on reset_password page
|
|
final_url = r.request.path
|
|
assert "/reset_password/" in final_url
|
|
|
|
# Get redirected to reset password page with token
|
|
r = client.get("/challenges", follow_redirects=False)
|
|
assert r.status_code == 302
|
|
assert "/reset_password/" in r.location
|
|
|
|
# Extract the token from the redirect URL
|
|
reset_url = r.location
|
|
token = reset_url.split("/reset_password/")[-1]
|
|
|
|
# Access the reset password page with the token
|
|
r = client.get(f"/reset_password/{token}")
|
|
assert r.status_code == 200
|
|
|
|
# Actually reset the password using the reset password form
|
|
with client.session_transaction() as sess:
|
|
reset_data = {"password": "newpassword123", "nonce": sess.get("nonce")}
|
|
|
|
# Submit the password reset form
|
|
r = client.post(f"/reset_password/{token}", data=reset_data)
|
|
assert r.status_code == 302 # Should redirect after successful reset
|
|
|
|
# Verify the password was actually changed and change_password flag was cleared
|
|
user = Users.query.filter_by(name="testuser").first()
|
|
assert user.change_password is False
|
|
assert verify_password("newpassword123", user.password)
|
|
|
|
client.get("/login")
|
|
with client.session_transaction() as sess:
|
|
data = {
|
|
"name": "testuser",
|
|
"password": "newpassword123",
|
|
"nonce": sess.get("nonce"),
|
|
}
|
|
|
|
r = client.post("/login", data=data)
|
|
assert r.status_code == 302
|
|
|
|
# Now user should be able to access protected routes normally
|
|
r = client.get("/challenges")
|
|
assert r.status_code == 200
|
|
assert r.location is None # No redirect
|
|
|
|
r = client.get("/profile")
|
|
assert r.status_code == 200
|
|
assert r.location is None # No redirect
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
def test_admin_can_set_change_password_via_api():
|
|
"""
|
|
Test that admins can set the change_password attribute via the API
|
|
"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Login as admin
|
|
client = login_as_user(app, name="admin", password="password")
|
|
|
|
# Create a user via API with change_password=True
|
|
r = client.post(
|
|
"/api/v1/users",
|
|
json={
|
|
"name": "apiuser",
|
|
"email": "apiuser@example.com",
|
|
"password": "password123",
|
|
"change_password": True,
|
|
},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
# Verify the user was created with change_password=True
|
|
response_data = r.get_json()
|
|
assert response_data["success"] is True
|
|
user_id = response_data["data"]["id"]
|
|
|
|
user = Users.query.filter_by(id=user_id).first()
|
|
assert user is not None
|
|
assert user.change_password is True
|
|
|
|
# Update user via API to set change_password=False
|
|
r = client.patch(f"/api/v1/users/{user_id}", json={"change_password": False})
|
|
assert r.status_code == 200
|
|
|
|
# Verify the change_password was updated
|
|
user = Users.query.filter_by(id=user_id).first()
|
|
assert user.change_password is False
|
|
|
|
# Test that non-admin users cannot set change_password via API
|
|
register_user(app, name="normaluser", email="normal@example.com")
|
|
normal_client = login_as_user(app, name="normaluser", password="password")
|
|
|
|
# Try to modify change_password on their own account (should fail)
|
|
normal_user = Users.query.filter_by(name="normaluser").first()
|
|
r = normal_client.patch(
|
|
f"/api/v1/users/{normal_user.id}",
|
|
json={
|
|
"change_password": True,
|
|
},
|
|
)
|
|
# Normal users shouldn't be able to modify change_password field
|
|
assert r.status_code == 403 # Forbidden
|
|
|
|
# Verify that change_password was not modified
|
|
normal_user = Users.query.filter_by(name="normaluser").first()
|
|
assert normal_user.change_password is False
|
|
|
|
# Also test via the /me endpoint
|
|
r = normal_client.patch(
|
|
"/api/v1/users/me",
|
|
json={
|
|
"change_password": True,
|
|
},
|
|
)
|
|
# Request goes through but doesn't actually modify the attribute
|
|
assert r.status_code == 200
|
|
|
|
# Verify that change_password was still not modified
|
|
normal_user = Users.query.filter_by(name="normaluser").first()
|
|
assert normal_user.change_password is False
|
|
|
|
destroy_ctfd(app)
|
|
|
|
|
|
@patch("smtplib.SMTP")
|
|
def test_user_reset_password_rate_limit(mock_smtp):
|
|
"""
|
|
Test that a user can only create 5 reset password attempts before they are rate limited
|
|
"""
|
|
app = create_ctfd()
|
|
with app.app_context():
|
|
# Create a user before configuring mail to not send any extra emails
|
|
register_user(app, name="user1", email="user@user.com")
|
|
|
|
# Set CTFd to send emails
|
|
set_config("mail_server", "localhost")
|
|
set_config("mail_port", 25)
|
|
set_config("mail_useauth", True)
|
|
set_config("mail_username", "username")
|
|
set_config("mail_password", "password")
|
|
|
|
with app.test_client() as client:
|
|
# Make 5 password reset requests (which should all succeed)
|
|
for _ in range(5):
|
|
client.get("/reset_password")
|
|
|
|
# Build reset password data
|
|
with client.session_transaction() as sess:
|
|
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
|
|
|
# Issue the password reset request
|
|
r = client.post("/reset_password", data=data)
|
|
assert r.status_code == 200
|
|
resp = r.get_data(as_text=True)
|
|
assert (
|
|
"If that account exists you will receive an email, please check your inbox"
|
|
in resp
|
|
)
|
|
assert "Too many password reset attempts" not in resp
|
|
|
|
# Verify that the email was sent 5 times
|
|
assert mock_smtp.return_value.send_message.call_count == 5
|
|
|
|
# 6th attempt should be rate limited
|
|
client.get("/reset_password")
|
|
with client.session_transaction() as sess:
|
|
data = {"nonce": sess.get("nonce"), "email": "user@user.com"}
|
|
|
|
r = client.post("/reset_password", data=data)
|
|
assert r.status_code == 200
|
|
resp = r.get_data(as_text=True)
|
|
assert "Too many password reset attempts. Please try again later." in resp
|
|
|
|
# Verify that no additional email was sent
|
|
assert mock_smtp.return_value.send_message.call_count == 5
|
|
|
|
destroy_ctfd(app)
|