Repository: CTFd/CTFd

Issue:
Info leak of number of solves after freeze
**Environment**:

  - CTFd Version/Commit: 2.1.3 / 75a9a5a6972b7f053a018a0845e0755894729801
  - Operating System: Amazon Linux 2 / CTFd running in docker container
  - Web Browser and Version: Firefox 68

**What happened?**

For challenges solved *after* freeze time, users can still click "Challenges", open a challenge, and then see the number of solves in the **non-opened** tab "Solves" (e.g. 2 solves as in the attached image below, both solves were made after freeze time).

![image](https://user-images.githubusercontent.com/197840/63594342-7e962680-c5b6-11e9-8911-f10c8cee81c8.png)

When the user **opens/clicks** the tab "Solves", the title will change to the number of solves before the freeze, e.g. 0 solves in the second attached image. Thus, there is an information leak about the number of solves of a challenge (as long as the user does not click the Solves tab).

![image](https://user-images.githubusercontent.com/197840/63594456-c452ef00-c5b6-11e9-8a81-76f612cc2f58.png)

Additionally, but not as important (I'm not sure about the desired behaviour here), after clicking the Solves tab, the user's *own* solves will not show in the tab.

**What did you expect to happen?**

Most importantly, the total number of solves should not be shown when first opening the challenge, since this leaks information about whether the challenge has been solved after the freeze.

Minor: perhaps the user's own solve should be shown, if they have solved it after the freeze? Not sure about the behaviour here though.

Thus, the "Solves" tab should show 1 or 0 solves, but definitely not 2 solves.

**How to reproduce your issue**

1. Create a CTF with a challenge and two users.
1. Freeze the CTF
1. Solve the challenge as user A
1. Solve the challenge as user B
1. As either user A or B, click the challenge, and notice that the "Solves" tab header shows "2 solves".
1. Click the "Solves" tab, and the tab header changes to 0 solves. This *maybe* should show 1 solve since the user has solved it? Or stay at 0 solves.

**Any associated stack traces or error logs**




Discussion:
Thanks for reporting this. I'll patch this out soon and issue a new release. 

Resolving patch:
diff --git a/CTFd/api/v1/challenges.py b/CTFd/api/v1/challenges.py
--- a/CTFd/api/v1/challenges.py
+++ b/CTFd/api/v1/challenges.py
@@ -38,7 +38,7 @@
 from CTFd.utils.user import get_current_team
 from CTFd.utils.user import get_current_user
 from CTFd.plugins.challenges import get_chal_class
-from CTFd.utils.dates import ctf_ended, ctf_paused, ctftime
+from CTFd.utils.dates import ctf_ended, ctf_paused, ctftime, unix_time_to_utc
 from CTFd.utils.logging import log
 from CTFd.utils.security.signing import serialize
 from sqlalchemy.sql import and_
@@ -267,8 +267,14 @@ def get(self, challenge_id):
                     Model.banned == False,
                     Model.hidden == False,
                 )
-                .count()
             )
+
+            # Only show solves that happened before freeze time if configured
+            freeze = get_config("freeze")
+            if not is_admin() and freeze:
+                solves = solves.filter(Solves.date < unix_time_to_utc(freeze))
+
+            solves = solves.count()
             response["solves"] = solves
         else:
             response["solves"] = None


Test patch:
diff --git a/tests/api/v1/test_challenges.py b/tests/api/v1/test_challenges.py
--- a/tests/api/v1/test_challenges.py
+++ b/tests/api/v1/test_challenges.py
@@ -536,14 +536,25 @@ def test_api_challenge_get_solves_ctf_frozen():
             chal2_id = chal2.id
 
         with freeze_time("2017-10-8"):
-            chal2 = gen_solve(app.db, user_id=2, challenge_id=chal2_id)
+            # User ID 2 solves Challenge ID 2
+            gen_solve(app.db, user_id=2, challenge_id=chal2_id)
+            # User ID 3 solves Challenge ID 1
+            gen_solve(app.db, user_id=3, challenge_id=chal_id)
+
+            # Challenge 1 has 2 solves
+            # Challenge 2 has 1 solve
 
             # There should now be two solves assigned to the same user.
-            assert Solves.query.count() == 2
+            assert Solves.query.count() == 3
 
             client = login_as_user(app, name="user2")
 
-            # Challenge 1 should have one solve
+            # Challenge 1 should have one solve (after freeze)
+            r = client.get("/api/v1/challenges/1")
+            data = r.get_json()["data"]
+            assert data['solves'] == 1
+
+            # Challenge 1 should have one solve (after freeze)
             r = client.get("/api/v1/challenges/1/solves")
             data = r.get_json()["data"]
             assert len(data) == 1