diff --git a/CTFd/teams.py b/CTFd/teams.py index f32be59e..f579dfcf 100644 --- a/CTFd/teams.py +++ b/CTFd/teams.py @@ -1,6 +1,6 @@ from flask import render_template, request, redirect, url_for, Blueprint from CTFd.models import db, Teams -from CTFd.utils.decorators import authed_only +from CTFd.utils.decorators import authed_only, ratelimit from CTFd.utils.decorators.modes import require_team_mode from CTFd.utils import config from CTFd.utils.user import get_current_user @@ -42,6 +42,7 @@ def listing(): @teams.route("/teams/join", methods=["GET", "POST"]) @authed_only @require_team_mode +@ratelimit(method="POST", limit=10, interval=5) def join(): if request.method == "GET": return render_template("teams/join_team.html") diff --git a/tests/teams/test_auth.py b/tests/teams/test_auth.py index 635fcc89..6c6abb83 100644 --- a/tests/teams/test_auth.py +++ b/tests/teams/test_auth.py @@ -78,3 +78,29 @@ def test_team_login(): r = client.get("/team") assert r.status_code == 200 destroy_ctfd(app) + + +def test_team_join_ratelimited(): + """Test that team joins are ratelimited""" + app = create_ctfd(user_mode="teams") + with app.app_context(): + gen_user(app.db, name="user") + gen_team(app.db, name="team") + with login_as_user(app) as client: + r = client.get("/teams/join") + assert r.status_code == 200 + with client.session_transaction() as sess: + data = { + "name": "team", + "password": "wrong_password", + "nonce": sess.get("nonce"), + } + for _ in range(10): + r = client.post("/teams/join", data=data) + + data["password"] = "password" + for _ in range(10): + r = client.post("/teams/join", data=data) + assert r.status_code == 429 + assert Users.query.filter_by(id=2).first().team_id is None + destroy_ctfd(app)