Repository: CTFd/CTFd
Pull request #439 (2017-11-08 06:53:01)

Preventing Hints from being unlocked after the end of a CTF 
Preventing Hints from being unlocked after the end of a CTF unless challenges can be viewed in which case they are available for purchase after the CTF. 

Hints are purchaseable during the CTF and when a CTF is frozen. 


Patch:
diff --git a/CTFd/challenges.py b/CTFd/challenges.py
index e27f3749..69b4c2f6 100644
--- a/CTFd/challenges.py
+++ b/CTFd/challenges.py
@@ -36,31 +36,27 @@ def hints_view(hintid):
                 'cost': hint.cost
             })
     elif request.method == 'POST':
-        if not unlock and utils.ctftime():
-            team = Teams.query.filter_by(id=session['id']).first()
-            if team.score() < hint.cost:
-                return jsonify({'errors': 'Not enough points'})
-            unlock = Unlocks(model='hints', teamid=session['id'], itemid=hint.id)
-            award = Awards(teamid=session['id'], name=text_type('Hint for {}'.format(chal.name)), value=(-hint.cost))
-            db.session.add(unlock)
-            db.session.add(award)
-            db.session.commit()
-            json_data = {
-                'hint': hint.hint,
-                'chal': hint.chal,
-                'cost': hint.cost
-            }
-            db.session.close()
-            return jsonify(json_data)
-        elif utils.ctf_ended():
-            json_data = {
-                'hint': hint.hint,
-                'chal': hint.chal,
-                'cost': hint.cost
-            }
-            db.session.close()
-            return jsonify(json_data)
-        else:
+        if unlock is None:  # The user does not have an unlock.
+            if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()):
+                # It's ctftime or the CTF has ended (but we allow views after)
+                team = Teams.query.filter_by(id=session['id']).first()
+                if team.score() < hint.cost:
+                    return jsonify({'errors': 'Not enough points'})
+                unlock = Unlocks(model='hints', teamid=session['id'], itemid=hint.id)
+                award = Awards(teamid=session['id'], name=text_type('Hint for {}'.format(chal.name)), value=(-hint.cost))
+                db.session.add(unlock)
+                db.session.add(award)
+                db.session.commit()
+                json_data = {
+                    'hint': hint.hint,
+                    'chal': hint.chal,
+                    'cost': hint.cost
+                }
+                db.session.close()
+                return jsonify(json_data)
+            elif utils.ctf_ended():  # The CTF has ended. No views after.
+                abort(403)
+        else:  # The user does have an unlock, we should give them their hint.
             json_data = {
                 'hint': hint.hint,
                 'chal': hint.chal,


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index e614363d..040c82e4 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -95,6 +95,7 @@ def gen_challenge(db, name='chal_name', description='chal_description', value=10
 
 def gen_award(db, teamid, name="award_name", value=100):
     award = Awards(teamid, name, value)
+    award.date = datetime.datetime.utcnow()
     db.session.add(award)
     db.session.commit()
     return award
diff --git a/tests/user/test_challenges.py b/tests/user/test_challenges.py
index 7c96ba54..83ad6324 100644
--- a/tests/user/test_challenges.py
+++ b/tests/user/test_challenges.py
@@ -271,7 +271,7 @@ def test_submitting_flags_with_large_ips():
 
 
 def test_unlocking_hints_with_no_cost():
-    '''Test that hints with no cost can be unlocked'''
+    """Test that hints with no cost can be unlocked"""
     app = create_ctfd()
     with app.app_context():
         register_user(app)
@@ -291,8 +291,110 @@ def test_unlocking_hints_with_no_cost():
     destroy_ctfd(app)
 
 
+def test_unlocking_hints_with_cost_during_ctf_with_points():
+    """Test that hints with a cost are unlocked if you have the points"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        chal = gen_challenge(app.db)
+        chal_id = chal.id
+        hint = gen_hint(app.db, chal_id, cost=10)
+        gen_award(app.db, teamid=2)
+
+        client = login_as_user(app)
+        with client.session_transaction() as sess:
+            data = {
+                "nonce": sess.get('nonce')
+            }
+        r = client.post('/hints/1', data=data)
+        output = r.get_data(as_text=True)
+        output = json.loads(output)
+        assert output.get('hint') == 'This is a hint'
+        user = Teams.query.filter_by(id=2).first()
+        assert user.score() == 90
+    destroy_ctfd(app)
+
+
+def test_unlocking_hints_with_cost_during_ctf_without_points():
+    """Test that hints with a cost are not unlocked if you don't have the points"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        chal = gen_challenge(app.db)
+        chal_id = chal.id
+        hint = gen_hint(app.db, chal_id, cost=10)
+
+        client = login_as_user(app)
+        with client.session_transaction() as sess:
+            data = {
+                "nonce": sess.get('nonce')
+            }
+        r = client.post('/hints/1', data=data)
+        output = r.get_data(as_text=True)
+        output = json.loads(output)
+        assert output.get('errors') == 'Not enough points'
+        user = Teams.query.filter_by(id=2).first()
+        assert user.score() == 0
+    destroy_ctfd(app)
+
+
+def test_unlocking_hints_with_cost_during_ended_ctf():
+    """Test that hints with a cost are not unlocked if the CTF has ended"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        chal = gen_challenge(app.db)
+        chal_id = chal.id
+        hint = gen_hint(app.db, chal_id, cost=10)
+        gen_award(app.db, teamid=2)
+
+        set_config('start', '1507089600')  # Wednesday, October 4, 2017 12:00:00 AM GMT-04:00 DST
+        set_config('end', '1507262400')  # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
+
+        with freeze_time("2017-11-4"):
+            client = login_as_user(app)
+            with client.session_transaction() as sess:
+                data = {
+                    "nonce": sess.get('nonce')
+                }
+            r = client.post('/hints/1', data=data)
+            assert r.status_code == 403
+            user = Teams.query.filter_by(id=2).first()
+            assert user.score() == 100
+            assert Unlocks.query.count() == 0
+    destroy_ctfd(app)
+
+
+def test_unlocking_hints_with_cost_during_frozen_ctf():
+    """Test that hints with a cost are unlocked if the CTF is frozen."""
+    app = create_ctfd()
+    with app.app_context():
+        set_config('freeze', '1507262400')  # Friday, October 6, 2017 12:00:00 AM GMT-04:00 DST
+        with freeze_time("2017-10-4"):
+            register_user(app)
+            chal = gen_challenge(app.db)
+            chal_id = chal.id
+            hint = gen_hint(app.db, chal_id, cost=10)
+            gen_award(app.db, teamid=2)
+
+        with freeze_time("2017-10-8"):
+            client = login_as_user(app)
+            with client.session_transaction() as sess:
+                data = {
+                    "nonce": sess.get('nonce')
+                }
+
+            r = client.post('/hints/1', data=data)
+            output = r.get_data(as_text=True)
+            output = json.loads(output)
+            assert output.get('hint') == 'This is a hint'
+            user = Teams.query.filter_by(id=2).first()
+            assert user.score() == 100
+    destroy_ctfd(app)
+
+
 def test_unlocking_hint_for_unicode_challenge():
-    '''Test that hints for challenges with unicode names can be unlocked'''
+    """Test that hints for challenges with unicode names can be unlocked"""
     app = create_ctfd()
     with app.app_context():
         register_user(app)