Repository: CTFd/CTFd
Pull request #379 (2017-09-08 03:29:01)

Top ten endpoint honors users with the same name
An anonymizing plugin revealed an issue where `/top/10` doesn't work properly if users share names. Since this could reasonably happen in a more production instance it bears fixing. 


Patch:
diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py
index 444c7ded..d509b3c6 100644
--- a/CTFd/scoreboard.py
+++ b/CTFd/scoreboard.py
@@ -87,7 +87,7 @@ def scores():
 
 @scoreboard.route('/top/<int:count>')
 def topteams(count):
-    json = {'scores': {}}
+    json = {'places': {}}
     if utils.get_config('view_scoreboard_if_authed') and not utils.authed():
         return redirect(url_for('auth.login', next=request.path))
     if utils.hide_scores():
@@ -98,7 +98,7 @@ def topteams(count):
 
     standings = get_standings(count=count)
 
-    for team in standings:
+    for i, team in enumerate(standings):
         solves = Solves.query.filter_by(teamid=team.teamid)
         awards = Awards.query.filter_by(teamid=team.teamid)
 
@@ -111,20 +111,24 @@ def topteams(count):
         solves = solves.all()
         awards = awards.all()
 
-        json['scores'][team.name] = []
+        json['places'][i + 1] = {
+            'id': team.teamid,
+            'name': team.name,
+            'solves': []
+        }
         for x in solves:
-            json['scores'][team.name].append({
+            json['places'][i + 1]['solves'].append({
                 'chal': x.chalid,
                 'team': x.teamid,
                 'value': x.chal.value,
                 'time': utils.unix_time(x.date)
             })
         for award in awards:
-            json['scores'][team.name].append({
+            json['places'][i + 1]['solves'].append({
                 'chal': None,
                 'team': award.teamid,
                 'value': award.value,
                 'time': utils.unix_time(award.date)
             })
-        json['scores'][team.name] = sorted(json['scores'][team.name], key=lambda k: k['time'])
+        json['places'][i + 1]['solves'] = sorted(json['places'][i + 1]['solves'], key=lambda k: k['time'])
     return jsonify(json)
diff --git a/CTFd/themes/original/static/js/scoreboard.js b/CTFd/themes/original/static/js/scoreboard.js
index e8b8c976..3c704983 100644
--- a/CTFd/themes/original/static/js/scoreboard.js
+++ b/CTFd/themes/original/static/js/scoreboard.js
@@ -24,20 +24,20 @@ function UTCtoDate(utc){
 }
 function scoregraph () {
     $.get(script_root + '/top/10', function( data ) {
-        var scores = $.parseJSON(JSON.stringify(data));
-        scores = scores['scores'];
-        if (Object.keys(scores).length == 0 ){
+        var places = $.parseJSON(JSON.stringify(data));
+        places = places['places'];
+        if (Object.keys(places).length == 0 ){
             return;
         }
 
-        var teams = Object.keys(scores);
+        var teams = Object.keys(places);
         var traces = [];
         for(var i = 0; i < teams.length; i++){
             var team_score = [];
             var times = [];
-            for(var j = 0; j < scores[teams[i]].length; j++){
-                team_score.push(scores[teams[i]][j].value);
-                var date = moment(scores[teams[i]][j].time * 1000);
+            for(var j = 0; j < places[teams[i]]['solves'].length; j++){
+                team_score.push(places[teams[i]]['solves'][j].value);
+                var date = moment(places[teams[i]]['solves'][j].time * 1000);
                 times.push(date.toDate());
             }
             team_score = cumulativesum(team_score);
@@ -45,7 +45,7 @@ function scoregraph () {
                 x: times,
                 y: team_score,
                 mode: 'lines+markers',
-                name: teams[i]
+                name: places[teams[i]]['name']
             }
             traces.push(trace);
         }


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index abc0defb..01e8222c 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -2,6 +2,7 @@ from CTFd import create_app
 from CTFd.models import *
 from sqlalchemy_utils import database_exists, create_database, drop_database
 from sqlalchemy.engine.url import make_url
+import datetime
 
 
 def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="password", setup=True):
@@ -110,6 +111,7 @@ def gen_hint(db, chal, hint="This is a hint", cost=0, type=0):
 
 def gen_solve(db, teamid, chalid, ip='127.0.0.1', flag='rightkey'):
     solve = Solves(teamid, chalid, ip, flag)
+    solve.date = datetime.datetime.utcnow()
     db.session.add(solve)
     db.session.commit()
     return solve
@@ -117,6 +119,7 @@ def gen_solve(db, teamid, chalid, ip='127.0.0.1', flag='rightkey'):
 
 def gen_wrongkey(db, teamid, chalid, ip='127.0.0.1', flag='wrongkey'):
     wrongkey = WrongKeys(teamid, chalid, ip, flag)
+    wrongkey.date = datetime.datetime.utcnow()
     db.session.add(wrongkey)
     db.session.commit()
     return wrongkey
diff --git a/tests/user/test_scoreboard.py b/tests/user/test_scoreboard.py
new file mode 100644
index 00000000..61d55a1d
--- /dev/null
+++ b/tests/user/test_scoreboard.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from CTFd.models import Teams, Solves, WrongKeys
+from CTFd.utils import get_config, set_config
+from CTFd import utils
+from tests.helpers import *
+from freezegun import freeze_time
+from mock import patch
+import json
+
+
+def test_top_10():
+    '''Make sure top10 returns correct information'''
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app, name="user1", email="user1@ctfd.io")
+        register_user(app, name="user2", email="user2@ctfd.io")
+
+        chal1 = gen_challenge(app.db)
+        flag1 = gen_flag(app.db, chal=chal1.id, flag='flag')
+        chal1_id = chal1.id
+
+        chal2 = gen_challenge(app.db)
+        flag2 = gen_flag(app.db, chal=chal2.id, flag='flag')
+        chal2_id = chal2.id
+
+        # Generates solve for user1
+        with freeze_time("2017-10-3 03:21:34"):
+            gen_solve(app.db, teamid=2, chalid=chal1_id)
+
+        with freeze_time("2017-10-4 03:25:45"):
+            gen_solve(app.db, teamid=2, chalid=chal2_id)
+
+        # Generate solve for user2
+        with freeze_time("2017-10-3 03:21:34"):
+            gen_solve(app.db, teamid=3, chalid=chal1_id)
+
+        client = login_as_user(app)
+        r = client.get('/top/10')
+        response = r.get_data(as_text=True)
+
+        saved = '''{
+          "places": {
+            "1": {
+              "id": 2,
+              "name": "user1",
+              "solves": [
+                {
+                  "chal": 1,
+                  "team": 2,
+                  "time": 1507000894,
+                  "value": 100
+                },
+                {
+                  "chal": 2,
+                  "team": 2,
+                  "time": 1507087545,
+                  "value": 100
+                }
+              ]
+            },
+            "2": {
+              "id": 3,
+              "name": "user2",
+              "solves": [
+                {
+                  "chal": 1,
+                  "team": 3,
+                  "time": 1507000894,
+                  "value": 100
+                }
+              ]
+            }
+          }
+        }'''
+        saved = json.loads(saved)
+        received = json.loads(response)
+        assert saved == received
+    destroy_ctfd(app)