vishalbhat07/ctfd
0
1#!/usr/bin/env python2# -*- coding: utf-8 -*-3 4from unittest.mock import patch5 6from freezegun import freeze_time7 8from CTFd.models import Users, db9from CTFd.utils import get_config, set_config10from CTFd.utils.crypto import verify_password11from tests.helpers import create_ctfd, destroy_ctfd, login_as_user, register_user12 13 14def test_register_user():15 """Can a user be registered"""16 app = create_ctfd()17 with app.app_context():18 register_user(app)19 user_count = Users.query.count()20 assert user_count == 2 # There's the admin user and the created user21 destroy_ctfd(app)22 23 24def test_register_unicode_user():25 """Can a user with a unicode name be registered"""26 app = create_ctfd()27 with app.app_context():28 register_user(app, name="你好")29 user_count = Users.query.count()30 assert user_count == 2 # There's the admin user and the created user31 destroy_ctfd(app)32 33 34def test_register_duplicate_username():35 """A user shouldn't be able to use an already registered team name"""36 app = create_ctfd()37 with app.app_context():38 register_user(39 app,40 name="user1",41 email="user1@examplectf.com",42 password="password",43 raise_for_error=False,44 )45 register_user(46 app,47 name="user1",48 email="user2@examplectf.com",49 password="password",50 raise_for_error=False,51 )52 register_user(53 app,54 name="admin ",55 email="admin2@examplectf.com",56 password="password",57 raise_for_error=False,58 )59 user_count = Users.query.count()60 assert user_count == 2 # There's the admin user and the first created user61 destroy_ctfd(app)62 63 64def test_register_duplicate_email():65 """A user shouldn't be able to use an already registered email address"""66 app = create_ctfd()67 with app.app_context():68 register_user(69 app,70 name="user1",71 email="user1@examplectf.com",72 password="password",73 raise_for_error=False,74 )75 register_user(76 app,77 name="user2",78 email="user1@examplectf.com",79 password="password",80 raise_for_error=False,81 )82 user_count = Users.query.count()83 assert user_count == 2 # There's the admin user and the first created user84 destroy_ctfd(app)85 86 87def test_register_whitelisted_email():88 """A user shouldn't be able to register with an email that isn't on the whitelist"""89 app = create_ctfd()90 with app.app_context():91 set_config(92 "domain_whitelist", "whitelisted.com, whitelisted.org, whitelisted.net"93 )94 register_user(95 app, name="not_whitelisted", email="user@nope.com", raise_for_error=False96 )97 assert Users.query.count() == 198 99 register_user(app, name="user1", email="user@whitelisted.com")100 assert Users.query.count() == 2101 102 register_user(app, name="user2", email="user@whitelisted.org")103 assert Users.query.count() == 3104 105 register_user(app, name="user3", email="user@whitelisted.net")106 assert Users.query.count() == 4107 destroy_ctfd(app)108 109 110def test_register_blacklisted_email():111 """A user shouldn't be able to register with an email that is on the blacklist"""112 app = create_ctfd()113 with app.app_context():114 set_config(115 "domain_blacklist", "blacklisted.com, blacklisted.org, blacklisted.net"116 )117 register_user(118 app, name="blacklisted", email="user@blacklisted.com", raise_for_error=False119 )120 assert Users.query.count() == 1121 122 register_user(app, name="user1", email="user@yep.com")123 assert Users.query.count() == 2124 125 register_user(app, name="user2", email="user@yay.org")126 assert Users.query.count() == 3127 128 register_user(app, name="user3", email="user@yipee.net")129 assert Users.query.count() == 4130 destroy_ctfd(app)131 132 133def test_user_bad_login():134 """A user should not be able to login with an incorrect password"""135 app = create_ctfd()136 with app.app_context():137 register_user(app)138 client = login_as_user(139 app, name="user", password="wrong_password", raise_for_error=False140 )141 with client.session_transaction() as sess:142 assert sess.get("id") is None143 r = client.get("/profile")144 assert r.location.startswith("/login") # We got redirected to login145 destroy_ctfd(app)146 147 148def test_user_login():149 """Can a registered user can login"""150 app = create_ctfd()151 with app.app_context():152 register_user(app)153 client = login_as_user(app)154 r = client.get("/profile")155 assert r.location is None # We didn't get redirected to login156 assert r.status_code == 200157 destroy_ctfd(app)158 159 160def test_user_login_with_email():161 """Can a registered user can login with an email address instead of a team name"""162 app = create_ctfd()163 with app.app_context():164 register_user(app)165 client = login_as_user(app, name="user@examplectf.com", password="password")166 r = client.get("/profile")167 assert r.location is None # We didn't get redirected to login168 assert r.status_code == 200169 destroy_ctfd(app)170 171 172def test_user_get_logout():173 """Can a registered user load /logout"""174 app = create_ctfd()175 with app.app_context():176 register_user(app)177 client = login_as_user(app)178 client.get("/logout", follow_redirects=True)179 r = client.get("/challenges")180 assert r.location == "/login?next=%2Fchallenges%3F"181 assert r.status_code == 302182 destroy_ctfd(app)183 184 185def test_user_isnt_admin():186 """A registered user cannot access admin pages"""187 app = create_ctfd()188 with app.app_context():189 register_user(app)190 client = login_as_user(app)191 for page in [192 "pages",193 "users",194 "teams",195 "scoreboard",196 "challenges",197 "statistics",198 "config",199 ]:200 r = client.get("/admin/{}".format(page))201 assert r.location.startswith("/login?next=")202 assert r.status_code == 302203 destroy_ctfd(app)204 205 206def test_expired_confirmation_links():207 """Test that expired confirmation links are reported to the user"""208 app = create_ctfd()209 with app.app_context():210 set_config("mail_server", "localhost")211 set_config("mail_port", 25)212 set_config("mail_useauth", True)213 set_config("mail_username", "username")214 set_config("mail_password", "password")215 set_config("verify_emails", True)216 217 register_user(app, email="user@user.com")218 client = login_as_user(app, name="user", password="password")219 220 # user@user.com "2012-01-14 03:21:34"221 confirm_link = (222 "http://localhost/confirm/bb8a8526146e50778b211ae63074595880edbc0b"223 )224 r = client.get(confirm_link)225 226 assert (227 "Your confirmation link is invalid, please generate a new one"228 in r.get_data(as_text=True)229 )230 user = Users.query.filter_by(email="user@user.com").first()231 assert user.verified is not True232 destroy_ctfd(app)233 234 235def test_invalid_confirmation_links():236 """Test that invalid confirmation links are reported to the user"""237 app = create_ctfd()238 with app.app_context():239 set_config("mail_server", "localhost")240 set_config("mail_port", 25)241 set_config("mail_useauth", True)242 set_config("mail_username", "username")243 set_config("mail_password", "password")244 set_config("verify_emails", True)245 246 register_user(app, email="user@user.com")247 client = login_as_user(app, name="user", password="password")248 249 # user@user.com "2012-01-14 03:21:34"250 confirm_link = "http://localhost/confirm/a8375iyu<script>alert(1)<script>hn3048wueorighkgnsfg"251 r = client.get(confirm_link)252 253 assert (254 "Your confirmation link is invalid, please generate a new one"255 in r.get_data(as_text=True)256 )257 user = Users.query.filter_by(email="user@user.com").first()258 assert user.verified is not True259 destroy_ctfd(app)260 261 262def test_expired_reset_password_link():263 """Test that expired reset password links are reported to the user"""264 app = create_ctfd()265 with app.app_context():266 set_config("mail_server", "localhost")267 set_config("mail_port", 25)268 set_config("mail_useauth", True)269 set_config("mail_username", "username")270 set_config("mail_password", "password")271 272 register_user(app, name="user1", email="user@user.com")273 274 with app.test_client() as client:275 forgot_link = "http://localhost/reset_password/bb8a8526146e50778b211ae63074595880edbc0b"276 r = client.get(forgot_link)277 278 assert (279 "Your reset link is invalid, please generate a new one"280 in r.get_data(as_text=True)281 )282 destroy_ctfd(app)283 284 285def test_invalid_reset_password_link():286 """Test that invalid reset password links are reported to the user"""287 app = create_ctfd()288 with app.app_context():289 set_config("mail_server", "localhost")290 set_config("mail_port", 25)291 set_config("mail_useauth", True)292 set_config("mail_username", "username")293 set_config("mail_password", "password")294 295 register_user(app, name="user1", email="user@user.com")296 297 with app.test_client() as client:298 # user@user.com "2012-01-14 03:21:34"299 forgot_link = "http://localhost/reset_password/5678ytfghjiu876tyfg<INVALID DATA>hvbnmkoi9u87y6trdf"300 r = client.get(forgot_link)301 302 assert (303 "Your reset link is invalid, please generate a new one"304 in r.get_data(as_text=True)305 )306 destroy_ctfd(app)307 308 309def test_contact_for_password_reset():310 """Test that if there is no mailserver configured, users should contact admins"""311 app = create_ctfd()312 with app.app_context():313 register_user(app, name="user1", email="user@user.com")314 315 with app.test_client() as client:316 forgot_link = "http://localhost/reset_password"317 r = client.get(forgot_link)318 319 assert "contact an organizer" in r.get_data(as_text=True)320 destroy_ctfd(app)321 322 323@patch("smtplib.SMTP")324def test_user_can_confirm_email(mock_smtp):325 """Test that a user is capable of confirming their email address"""326 app = create_ctfd()327 with app.app_context(), freeze_time("2012-01-14 03:21:34"):328 # Set CTFd to only allow confirmed users and send emails329 set_config("verify_emails", True)330 set_config("mail_server", "localhost")331 set_config("mail_port", 25)332 set_config("mail_useauth", True)333 set_config("mail_username", "username")334 set_config("mail_password", "password")335 336 register_user(app, name="user1", email="user@user.com")337 338 # Teams are not verified by default339 user = Users.query.filter_by(email="user@user.com").first()340 assert user.verified is False341 342 client = login_as_user(app, name="user1", password="password")343 344 r = client.get("/confirm")345 assert "We've sent a confirmation email" in r.get_data(as_text=True)346 347 # smtp send message function was called348 mock_smtp.return_value.send_message.assert_called()349 350 with client.session_transaction() as sess:351 urandom_value = b"\xff" * 32352 with patch("os.urandom", return_value=urandom_value):353 data = {"nonce": sess.get("nonce")}354 r = client.post("http://localhost/confirm", data=data)355 assert "Confirmation email sent to" in r.get_data(as_text=True)356 357 r = client.get("/challenges")358 assert r.location == "/confirm" # We got redirected to /confirm359 360 r = client.get(361 "http://localhost/confirm/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"362 )363 assert r.location == "/challenges"364 365 # The team is now verified366 user = Users.query.filter_by(email="user@user.com").first()367 assert user.verified is True368 369 r = client.get("http://localhost/confirm")370 assert r.location == "/settings"371 destroy_ctfd(app)372 373 374@patch("smtplib.SMTP")375def test_user_can_reset_password(mock_smtp):376 """Test that a user is capable of resetting their password"""377 from email.message import EmailMessage378 379 app = create_ctfd()380 with app.app_context():381 # Set CTFd to send emails382 set_config("mail_server", "localhost")383 set_config("mail_port", 25)384 set_config("mail_useauth", True)385 set_config("mail_username", "username")386 set_config("mail_password", "password")387 388 # Create a user389 register_user(app, name="user1", email="user@user.com")390 391 with app.test_client() as client:392 client.get("/reset_password")393 394 # Build reset password data395 with client.session_transaction() as sess:396 data = {"nonce": sess.get("nonce"), "email": "user@user.com"}397 398 # Issue the password reset request399 urandom_value = b"\xff" * 32400 with patch("os.urandom", return_value=urandom_value):401 client.post("/reset_password", data=data)402 403 ctf_name = get_config("ctf_name")404 from_addr = get_config("mailfrom_addr") or app.config.get("MAILFROM_ADDR")405 from_addr = "{} <{}>".format(ctf_name, from_addr)406 407 to_addr = "user@user.com"408 409 # Build the email410 msg = (411 "Did you initiate a password reset on CTFd? If you didn't initiate this request you can ignore this email. "412 "\n\nClick the following link to reset your password:\n"413 "http://localhost/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\n\n"414 "If the link is not clickable, try copying and pasting it into your browser."415 )416 ctf_name = get_config("ctf_name")417 418 email_msg = EmailMessage()419 email_msg.set_content(msg)420 421 email_msg["Subject"] = "Password Reset Request from {ctf_name}".format(422 ctf_name=ctf_name423 )424 email_msg["From"] = from_addr425 email_msg["To"] = to_addr426 427 # Make sure that the reset password email is sent428 mock_smtp.return_value.send_message.assert_called()429 assert str(mock_smtp.return_value.send_message.call_args[0][0]) == str(430 email_msg431 )432 433 # Get user's original password434 user = Users.query.filter_by(email="user@user.com").first()435 436 # Build the POST data437 with client.session_transaction() as sess:438 data = {"nonce": sess.get("nonce"), "password": "passwordtwo"}439 440 # Do the password reset441 client.get(442 "/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"443 )444 client.post(445 "/reset_password/ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",446 data=data,447 )448 449 # Make sure that the user's password changed450 user = Users.query.filter_by(email="user@user.com").first()451 assert verify_password("passwordtwo", user.password)452 destroy_ctfd(app)453 454 455def test_banned_user():456 app = create_ctfd()457 with app.app_context():458 register_user(app)459 client = login_as_user(app)460 user = Users.query.filter_by(id=2).first()461 user.banned = True462 db.session.commit()463 464 routes = ["/", "/challenges", "/api/v1/challenges"]465 for route in routes:466 r = client.get(route)467 assert r.status_code == 403468 destroy_ctfd(app)469 470 471def test_registration_code_required():472 """473 Test that registration code configuration properly blocks logins474 with missing and incorrect registration codes475 """476 app = create_ctfd()477 with app.app_context():478 # Set a registration code479 set_config("registration_code", "secret-sauce")480 481 with app.test_client() as client:482 # Load CSRF nonce483 r = client.get("/register")484 resp = r.get_data(as_text=True)485 assert "Registration Code" in resp486 with client.session_transaction() as sess:487 data = {488 "name": "user",489 "email": "user1@examplectf.com",490 "password": "password",491 "nonce": sess.get("nonce"),492 }493 # Attempt registration without password494 r = client.post("/register", data=data)495 resp = r.get_data(as_text=True)496 assert "The registration code you entered was incorrect" in resp497 498 # Attempt registration with wrong password499 data["registration_code"] = "wrong-sauce"500 r = client.post("/register", data=data)501 resp = r.get_data(as_text=True)502 assert "The registration code you entered was incorrect" in resp503 504 # Attempt registration with right password505 data["registration_code"] = "secret-sauce"506 r = client.post("/register", data=data)507 assert r.status_code == 302508 assert r.location.startswith("/challenges")509 destroy_ctfd(app)510 511 512def test_registration_code_allows_numeric():513 """514 Test that registration code is allowed to be all numeric515 """516 app = create_ctfd()517 with app.app_context():518 # Set a registration code519 set_config("registration_code", "1234567890")520 521 with app.test_client() as client:522 # Load CSRF nonce523 r = client.get("/register")524 resp = r.get_data(as_text=True)525 assert "Registration Code" in resp526 with client.session_transaction() as sess:527 data = {528 "name": "user",529 "email": "user1@examplectf.com",530 "password": "password",531 "nonce": sess.get("nonce"),532 }533 534 # Attempt registration with numeric registration code535 data["registration_code"] = "1234567890"536 r = client.post("/register", data=data)537 assert r.status_code == 302538 assert r.location.startswith("/challenges")539 destroy_ctfd(app)540 541 542def test_registration_password_minimum_length():543 """544 Test that registration enforces minimum password length when configured545 """546 app = create_ctfd()547 with app.app_context():548 # Set a minimum password length549 set_config("password_min_length", 8)550 551 with app.test_client() as client:552 # Load CSRF nonce553 r = client.get("/register")554 with client.session_transaction() as sess:555 data = {556 "name": "user",557 "email": "user1@examplectf.com",558 "password": "short", # Only 5 characters559 "nonce": sess.get("nonce"),560 }561 562 # Attempt registration with password too short563 r = client.post("/register", data=data)564 resp = r.get_data(as_text=True)565 assert "Password must be at least 8 characters" in resp566 assert r.status_code == 200 # Should stay on registration page567 568 # Verify user was not created569 user_count = Users.query.count()570 assert user_count == 1 # Only admin user exists571 572 # Attempt registration with password meeting minimum length573 data["password"] = "validpassword" # 13 characters, meets minimum574 r = client.post("/register", data=data)575 assert r.status_code == 302576 assert r.location.startswith("/challenges")577 578 # Verify user was created579 user_count = Users.query.count()580 assert user_count == 2 # Admin user + new user581 582 # Test with minimum length set to 0 (disabled)583 set_config("password_min_length", 0)584 585 with app.test_client() as client:586 # Load CSRF nonce587 r = client.get("/register")588 with client.session_transaction() as sess:589 data = {590 "name": "user2",591 "email": "user2@examplectf.com",592 "password": "x", # Only 1 character593 "nonce": sess.get("nonce"),594 }595 596 # Should allow short password when minimum length is 0597 r = client.post("/register", data=data)598 assert r.status_code == 302599 assert r.location.startswith("/challenges")600 601 # Verify user was created602 user_count = Users.query.count()603 assert user_count == 3 # Admin user + 2 new users604 destroy_ctfd(app)605 606 607def test_user_change_password_required():608 """609 Test that users with change_password=True are redirected to reset password610 and cannot access other pages until they change their password611 """612 app = create_ctfd()613 with app.app_context():614 # Create a user with change_password=True615 register_user(616 app, name="testuser", email="test@example.com", password="oldpassword"617 )618 user = Users.query.filter_by(name="testuser").first()619 user.change_password = True620 db.session.commit()621 622 with app.test_client() as client:623 # Login as the user624 with client.session_transaction() as sess:625 data = {626 "name": "testuser",627 "password": "oldpassword",628 "nonce": sess.get("nonce"),629 }630 631 # Get login page first to get nonce632 client.get("/login")633 with client.session_transaction() as sess:634 data["nonce"] = sess.get("nonce")635 636 # Login637 r = client.post("/login", data=data)638 assert r.status_code == 302639 640 # Test that user is redirected to reset_password when accessing various pages641 protected_routes = [642 "/",643 "/challenges",644 "/scoreboard",645 "/profile",646 "/settings",647 "/api/v1/challenges",648 ]649 650 for route in protected_routes:651 r = client.get(route, follow_redirects=False)652 # Should be redirected to reset_password with a token653 assert r.status_code == 302654 assert "/reset_password/" in r.location655 656 # Test that the user can access the reset_password page directly657 r = client.get(route, follow_redirects=True)658 # Should end up on reset_password page659 final_url = r.request.path660 assert "/reset_password/" in final_url661 662 # Get redirected to reset password page with token663 r = client.get("/challenges", follow_redirects=False)664 assert r.status_code == 302665 assert "/reset_password/" in r.location666 667 # Extract the token from the redirect URL668 reset_url = r.location669 token = reset_url.split("/reset_password/")[-1]670 671 # Access the reset password page with the token672 r = client.get(f"/reset_password/{token}")673 assert r.status_code == 200674 675 # Actually reset the password using the reset password form676 with client.session_transaction() as sess:677 reset_data = {"password": "newpassword123", "nonce": sess.get("nonce")}678 679 # Submit the password reset form680 r = client.post(f"/reset_password/{token}", data=reset_data)681 assert r.status_code == 302 # Should redirect after successful reset682 683 # Verify the password was actually changed and change_password flag was cleared684 user = Users.query.filter_by(name="testuser").first()685 assert user.change_password is False686 assert verify_password("newpassword123", user.password)687 688 client.get("/login")689 with client.session_transaction() as sess:690 data = {691 "name": "testuser",692 "password": "newpassword123",693 "nonce": sess.get("nonce"),694 }695 696 r = client.post("/login", data=data)697 assert r.status_code == 302698 699 # Now user should be able to access protected routes normally700 r = client.get("/challenges")701 assert r.status_code == 200702 assert r.location is None # No redirect703 704 r = client.get("/profile")705 assert r.status_code == 200706 assert r.location is None # No redirect707 708 destroy_ctfd(app)709 710 711def test_admin_can_set_change_password_via_api():712 """713 Test that admins can set the change_password attribute via the API714 """715 app = create_ctfd()716 with app.app_context():717 # Login as admin718 client = login_as_user(app, name="admin", password="password")719 720 # Create a user via API with change_password=True721 r = client.post(722 "/api/v1/users",723 json={724 "name": "apiuser",725 "email": "apiuser@example.com",726 "password": "password123",727 "change_password": True,728 },729 )730 assert r.status_code == 200731 732 # Verify the user was created with change_password=True733 response_data = r.get_json()734 assert response_data["success"] is True735 user_id = response_data["data"]["id"]736 737 user = Users.query.filter_by(id=user_id).first()738 assert user is not None739 assert user.change_password is True740 741 # Update user via API to set change_password=False742 r = client.patch(f"/api/v1/users/{user_id}", json={"change_password": False})743 assert r.status_code == 200744 745 # Verify the change_password was updated746 user = Users.query.filter_by(id=user_id).first()747 assert user.change_password is False748 749 # Test that non-admin users cannot set change_password via API750 register_user(app, name="normaluser", email="normal@example.com")751 normal_client = login_as_user(app, name="normaluser", password="password")752 753 # Try to modify change_password on their own account (should fail)754 normal_user = Users.query.filter_by(name="normaluser").first()755 r = normal_client.patch(756 f"/api/v1/users/{normal_user.id}",757 json={758 "change_password": True,759 },760 )761 # Normal users shouldn't be able to modify change_password field762 assert r.status_code == 403 # Forbidden763 764 # Verify that change_password was not modified765 normal_user = Users.query.filter_by(name="normaluser").first()766 assert normal_user.change_password is False767 768 # Also test via the /me endpoint769 r = normal_client.patch(770 "/api/v1/users/me",771 json={772 "change_password": True,773 },774 )775 # Request goes through but doesn't actually modify the attribute776 assert r.status_code == 200777 778 # Verify that change_password was still not modified779 normal_user = Users.query.filter_by(name="normaluser").first()780 assert normal_user.change_password is False781 782 destroy_ctfd(app)783 784 785@patch("smtplib.SMTP")786def test_user_reset_password_rate_limit(mock_smtp):787 """788 Test that a user can only create 5 reset password attempts before they are rate limited789 """790 app = create_ctfd()791 with app.app_context():792 # Create a user before configuring mail to not send any extra emails793 register_user(app, name="user1", email="user@user.com")794 795 # Set CTFd to send emails796 set_config("mail_server", "localhost")797 set_config("mail_port", 25)798 set_config("mail_useauth", True)799 set_config("mail_username", "username")800 set_config("mail_password", "password")801 802 with app.test_client() as client:803 # Make 5 password reset requests (which should all succeed)804 for _ in range(5):805 client.get("/reset_password")806 807 # Build reset password data808 with client.session_transaction() as sess:809 data = {"nonce": sess.get("nonce"), "email": "user@user.com"}810 811 # Issue the password reset request812 r = client.post("/reset_password", data=data)813 assert r.status_code == 200814 resp = r.get_data(as_text=True)815 assert (816 "If that account exists you will receive an email, please check your inbox"817 in resp818 )819 assert "Too many password reset attempts" not in resp820 821 # Verify that the email was sent 5 times822 assert mock_smtp.return_value.send_message.call_count == 5823 824 # 6th attempt should be rate limited825 client.get("/reset_password")826 with client.session_transaction() as sess:827 data = {"nonce": sess.get("nonce"), "email": "user@user.com"}828 829 r = client.post("/reset_password", data=data)830 assert r.status_code == 200831 resp = r.get_data(as_text=True)832 assert "Too many password reset attempts. Please try again later." in resp833 834 # Verify that no additional email was sent835 assert mock_smtp.return_value.send_message.call_count == 5836 837 destroy_ctfd(app)838 