Repository: CTFd/CTFd
Pull request #827 (2019-01-11 00:47:11)

Require CSRF-Token header on state changing API requests
* Require CSRF-Token header on state changing API requests
* Require CSRF nonces on more than just POSTs, 
* Replace usage of `fetch()` with custom `CTFd.fetch()` implementation


codecov[bot]: # [Codecov](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=h1) Report
> Merging [#827](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=desc) into [master](https://codecov.io/gh/CTFd/CTFd/commit/9ee743de7e180133b7f85c12af934dea1969b3a5?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/CTFd/CTFd/pull/827/graphs/tree.svg?width=650&token=IfPqkWkt9A&height=150&src=pr)](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #827      +/-   ##
==========================================
- Coverage   84.82%   84.81%   -0.02%     
==========================================
  Files          91       91              
  Lines        4560     4563       +3     
==========================================
+ Hits         3868     3870       +2     
- Misses        692      693       +1
```


| [Impacted Files](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [CTFd/utils/initialization/\_\_init\_\_.py](https://codecov.io/gh/CTFd/CTFd/pull/827/diff?src=pr&el=tree#diff-Q1RGZC91dGlscy9pbml0aWFsaXphdGlvbi9fX2luaXRfXy5weQ==) | `94.94% <100%> (+0.15%)` | :arrow_up: |
| [CTFd/utils/decorators/\_\_init\_\_.py](https://codecov.io/gh/CTFd/CTFd/pull/827/diff?src=pr&el=tree#diff-Q1RGZC91dGlscy9kZWNvcmF0b3JzL19faW5pdF9fLnB5) | `85.18% <0%> (-1.24%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=footer). Last update [9ee743d...d358442](https://codecov.io/gh/CTFd/CTFd/pull/827?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CTFd/plugins/challenges/assets/view.js b/CTFd/plugins/challenges/assets/view.js
index 3efaea04..4d9d836c 100644
--- a/CTFd/plugins/challenges/assets/view.js
+++ b/CTFd/plugins/challenges/assets/view.js
@@ -32,7 +32,7 @@ window.challenge.submit = function (cb, preview) {
         'submission': submission
     };
 
-    fetch(script_root + url, {
+    CTFd.fetch(url, {
         method: 'POST',
         credentials: 'same-origin',
         headers: {
diff --git a/CTFd/plugins/dynamic_challenges/assets/view.js b/CTFd/plugins/dynamic_challenges/assets/view.js
index 3efaea04..4d9d836c 100644
--- a/CTFd/plugins/dynamic_challenges/assets/view.js
+++ b/CTFd/plugins/dynamic_challenges/assets/view.js
@@ -32,7 +32,7 @@ window.challenge.submit = function (cb, preview) {
         'submission': submission
     };
 
-    fetch(script_root + url, {
+    CTFd.fetch(url, {
         method: 'POST',
         credentials: 'same-origin',
         headers: {
diff --git a/CTFd/themes/admin/static/js/challenges/challenge.js b/CTFd/themes/admin/static/js/challenges/challenge.js
index d6813b42..309829fd 100644
--- a/CTFd/themes/admin/static/js/challenges/challenge.js
+++ b/CTFd/themes/admin/static/js/challenges/challenge.js
@@ -143,9 +143,12 @@ $(document).ready(function () {
             title: "Delete Challenge",
             body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(CHALLENGE_NAME) + "</strong>"),
             success: function () {
-                var route = script_root + '/api/v1/challenges/' + CHALLENGE_ID;
-                $.delete(route, {}, function (data) {
-                    if (data.success) {
+                CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
+                    if (response.success) {
                         window.location = script_root + '/admin/challenges';
                     }
                 });
@@ -159,7 +162,7 @@ $(document).ready(function () {
         console.log(params);
 
 
-        fetch(script_root + '/api/v1/challenges/' + CHALLENGE_ID, {
+        CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
             method: 'PATCH',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/admin/static/js/challenges/files.js b/CTFd/themes/admin/static/js/challenges/files.js
index f5327e39..3ab9be56 100644
--- a/CTFd/themes/admin/static/js/challenges/files.js
+++ b/CTFd/themes/admin/static/js/challenges/files.js
@@ -27,14 +27,16 @@ $(document).ready(function () {
             title: "Delete Files",
             body: "Are you sure you want to delete this file?",
             success: function () {
-                $.delete(script_root + '/api/v1/files/' + file_id, {}, function (data) {
-                    if (data.success) {
-                       row.remove();
+                CTFd.fetch('/api/v1/files/' + file_id, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
+                    if (response.success) {
+                        row.remove();
                     }
                 });
             }
         });
-
-
     });
 });
\ No newline at end of file
diff --git a/CTFd/themes/admin/static/js/challenges/flags.js b/CTFd/themes/admin/static/js/challenges/flags.js
index 97db7a03..2a3abd81 100644
--- a/CTFd/themes/admin/static/js/challenges/flags.js
+++ b/CTFd/themes/admin/static/js/challenges/flags.js
@@ -21,7 +21,7 @@ $(document).ready(function () {
             e.preventDefault();
             var params = $(this).serializeJSON(true);
             params['challenge'] = CHALLENGE_ID;
-            fetch(script_root + '/api/v1/flags', {
+            CTFd.fetch('/api/v1/flags', {
                 method: 'POST',
                 credentials: 'same-origin',
                 headers: {
@@ -70,7 +70,7 @@ $(document).ready(function () {
                     e.preventDefault();
                     var params = $('#edit-flags form').serializeJSON();
 
-                    fetch(script_root + '/api/v1/flags/' + flag_id, {
+                    CTFd.fetch('/api/v1/flags/' + flag_id, {
                         method: 'PATCH',
                         credentials: 'same-origin',
                         headers: {
@@ -101,9 +101,12 @@ $(document).ready(function () {
             title: "Delete Flag",
             body: "Are you sure you want to delete this flag?",
             success: function () {
-                var route = script_root + '/api/v1/flags/' + flag_id;
-                $.delete(route, {}, function (data) {
-                    if (data.success) {
+                CTFd.fetch('/api/v1/flags/' + flag_id, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
+                    if (response.success) {
                         row.remove();
                     }
                 });
diff --git a/CTFd/themes/admin/static/js/challenges/hints.js b/CTFd/themes/admin/static/js/challenges/hints.js
index e006ce2c..f0088b72 100644
--- a/CTFd/themes/admin/static/js/challenges/hints.js
+++ b/CTFd/themes/admin/static/js/challenges/hints.js
@@ -1,5 +1,5 @@
 function hint(id) {
-    return fetch(script_root + '/api/v1/hints/' + id + '?preview=true', {
+    return CTFd.fetch('/api/v1/hints/' + id + '?preview=true', {
         method: 'GET',
         credentials: 'same-origin',
         headers: {
@@ -61,9 +61,12 @@ $(document).ready(function () {
             title: "Delete Hint",
             body: "Are you sure you want to delete this hint?",
             success: function () {
-                var route = script_root + '/api/v1/hints/' + hint_id;
-                $.delete(route, {}, function (data) {
-                    if (data.success) {
+                CTFd.fetch('/api/v1/hints/' + hint_id, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
+                    if (response.success) {
                         row.remove();
                     }
                 });
@@ -75,7 +78,7 @@ $(document).ready(function () {
         e.preventDefault();
         var hint_id = $(this).attr('hint-id');
 
-        fetch(script_root + '/api/v1/hints/' + hint_id + '?preview=true', {
+        CTFd.fetch('/api/v1/hints/' + hint_id + '?preview=true', {
             method: 'GET',
             credentials: 'same-origin',
             headers: {
@@ -119,7 +122,7 @@ $(document).ready(function () {
             method = 'PATCH';
             url = '/api/v1/hints/' + params.id;
         }
-        fetch(script_root + url, {
+        CTFd.fetch(url, {
             method: method,
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/admin/static/js/challenges/new.js b/CTFd/themes/admin/static/js/challenges/new.js
index 0785434c..976b54ea 100644
--- a/CTFd/themes/admin/static/js/challenges/new.js
+++ b/CTFd/themes/admin/static/js/challenges/new.js
@@ -13,7 +13,7 @@ function load_chal_template(challenge){
                 $("#create-chal-entry-div form").submit(function (e) {
                     e.preventDefault();
                     var params = $("#create-chal-entry-div form").serializeJSON();
-                    fetch(script_root + '/api/v1/challenges', {
+                    CTFd.fetch('/api/v1/challenges', {
                         method: 'POST',
                         credentials: 'same-origin',
                         headers: {
diff --git a/CTFd/themes/admin/static/js/challenges/requirements.js b/CTFd/themes/admin/static/js/challenges/requirements.js
index fc80a81f..a5c593fb 100644
--- a/CTFd/themes/admin/static/js/challenges/requirements.js
+++ b/CTFd/themes/admin/static/js/challenges/requirements.js
@@ -10,7 +10,7 @@ $(document).ready(function () {
             'requirements': CHALLENGE_REQUIREMENTS
         };
 
-        fetch(script_root + '/api/v1/challenges/' + CHALLENGE_ID, {
+        CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
             method: 'PATCH',
             credentials: 'same-origin',
             headers: {
@@ -38,7 +38,7 @@ $(document).ready(function () {
             'requirements': CHALLENGE_REQUIREMENTS
         };
 
-        fetch(script_root + '/api/v1/challenges/' + CHALLENGE_ID, {
+        CTFd.fetch('/api/v1/challenges/' + CHALLENGE_ID, {
             method: 'PATCH',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/admin/static/js/challenges/tags.js b/CTFd/themes/admin/static/js/challenges/tags.js
index f66b8570..189484e2 100644
--- a/CTFd/themes/admin/static/js/challenges/tags.js
+++ b/CTFd/themes/admin/static/js/challenges/tags.js
@@ -1,10 +1,16 @@
 function delete_tag(elem){
     var elem = $(elem);
     var tag_id = elem.attr('tag-id');
-    $.delete(script_root + '/api/v1/tags/' + tag_id, function(){
-        $(elem).parent().remove()
-    });
 
+    CTFd.fetch('/api/v1/tags/' + tag_id, {
+        method: 'DELETE',
+    }).then(function (response) {
+        return response.json();
+    }).then(function (response) {
+        if (response.success) {
+            $(elem).parent().remove()
+        }
+    });
 }
 
 $(document).ready(function () {
@@ -16,7 +22,7 @@ $(document).ready(function () {
                 challenge: CHALLENGE_ID
             };
 
-            fetch(script_root + '/api/v1/tags', {
+            CTFd.fetch('/api/v1/tags', {
                 method: 'POST',
                 credentials: 'same-origin',
                 headers: {
diff --git a/CTFd/themes/admin/static/js/configs/configs.js b/CTFd/themes/admin/static/js/configs/configs.js
index 249d7e49..855e2fe4 100644
--- a/CTFd/themes/admin/static/js/configs/configs.js
+++ b/CTFd/themes/admin/static/js/configs/configs.js
@@ -105,7 +105,7 @@ function update_configs(obj){
         }
     });
 
-    fetch(script_root + target, {
+    CTFd.fetch(target, {
         method: method,
         credentials: 'same-origin',
         headers: {
@@ -127,7 +127,7 @@ function upload_logo(form) {
             var params = {
                 'value': upload.location
             };
-            fetch(script_root + '/api/v1/configs/ctf_logo', {
+            CTFd.fetch('/api/v1/configs/ctf_logo', {
                 method: 'PATCH',
                 credentials: 'same-origin',
                 headers: {
@@ -160,7 +160,7 @@ function remove_logo() {
             var params = {
                 'value': null
             };
-            fetch(script_root + '/api/v1/configs/ctf_logo', {
+            CTFd.fetch('/api/v1/configs/ctf_logo', {
                 method: 'PATCH',
                 credentials: 'same-origin',
                 headers: {
diff --git a/CTFd/themes/admin/static/js/notifications/notifications.js b/CTFd/themes/admin/static/js/notifications/notifications.js
index 26886a7f..eb3f8b69 100644
--- a/CTFd/themes/admin/static/js/notifications/notifications.js
+++ b/CTFd/themes/admin/static/js/notifications/notifications.js
@@ -4,7 +4,7 @@ $(document).ready(function () {
         var form = $('#notifications_form');
         var params = form.serializeJSON();
 
-        fetch(script_root + '/api/v1/notifications', {
+        CTFd.fetch('/api/v1/notifications', {
             method: 'POST',
             credentials: 'same-origin',
             headers: {
@@ -32,8 +32,11 @@ $(document).ready(function () {
             title: 'Delete Notification',
             body: "Are you sure you want to delete this notification?",
             success: function () {
-                var delete_route = script_root + '/api/v1/notifications/' + notif_id;
-                $.delete(delete_route, {}, function (response) {
+                CTFd.fetch('/api/v1/notifications/' + notif_id, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
                     if (response.success) {
                         elem.parent().remove();
                     }
diff --git a/CTFd/themes/admin/static/js/pages/editor.js b/CTFd/themes/admin/static/js/pages/editor.js
index a3652699..8becd239 100644
--- a/CTFd/themes/admin/static/js/pages/editor.js
+++ b/CTFd/themes/admin/static/js/pages/editor.js
@@ -90,7 +90,7 @@ function submit_form() {
         method = 'PATCH';
     }
 
-    fetch(script_root + target, {
+    CTFd.fetch(target, {
         method: method,
         credentials: 'same-origin',
         headers: {
diff --git a/CTFd/themes/admin/static/js/pages/media.js b/CTFd/themes/admin/static/js/pages/media.js
index 1acdabb2..7165c41a 100644
--- a/CTFd/themes/admin/static/js/pages/media.js
+++ b/CTFd/themes/admin/static/js/pages/media.js
@@ -49,8 +49,8 @@ function get_filetype_icon_class(filename){
 }
 
 function get_page_files(){
-    return fetch(
-        script_root + '/api/v1/files?type=page', {
+    return CTFd.fetch(
+        '/api/v1/files?type=page', {
             credentials: 'same-origin',
         }
     ).then(function (response) {
diff --git a/CTFd/themes/admin/static/js/pages/pages.js b/CTFd/themes/admin/static/js/pages/pages.js
index 74d0c77c..78b4f56d 100644
--- a/CTFd/themes/admin/static/js/pages/pages.js
+++ b/CTFd/themes/admin/static/js/pages/pages.js
@@ -9,8 +9,11 @@ $(document).ready(function () {
                 "<strong>" + htmlentities(name) + "</strong>"
             ),
             success: function () {
-                var page_delete_route = '{{ request.script_root }}/admin/pages/delete';
-                $.delete(script_root + '/api/v1/pages/' + page_id, {}, function (response) {
+                CTFd.fetch('/api/v1/pages/' + page_id, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
                     if (response.success) {
                         elem.parent().parent().remove();
                     }
diff --git a/CTFd/themes/admin/static/js/scoreboard/scoreboard.js b/CTFd/themes/admin/static/js/scoreboard/scoreboard.js
index 68559e20..67a19adf 100644
--- a/CTFd/themes/admin/static/js/scoreboard/scoreboard.js
+++ b/CTFd/themes/admin/static/js/scoreboard/scoreboard.js
@@ -13,7 +13,7 @@ function toggle_account(elem) {
         'hidden': hidden
     };
 
-    fetch(script_root + '/api/v1/'+ user_mode +'/' + teamId, {
+    CTFd.fetch('/api/v1/'+ user_mode +'/' + teamId, {
         method: 'PATCH',
         credentials: 'same-origin',
         headers: {
diff --git a/CTFd/themes/admin/static/js/submissions/submissions.js b/CTFd/themes/admin/static/js/submissions/submissions.js
index 2e31aa1c..26c06dfc 100644
--- a/CTFd/themes/admin/static/js/submissions/submissions.js
+++ b/CTFd/themes/admin/static/js/submissions/submissions.js
@@ -17,8 +17,11 @@ $(document).ready(function () {
                 "<strong>" + htmlentities(chal_name) + "</strong>"
             ),
             success: function () {
-                var route = script_root + '/api/v1/submissions/' + key_id;
-                $.delete(route, function (response) {
+                CTFd.fetch('/api/v1/submissions/' + key_id, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
                     if (response.success) {
                         td_row.remove();
                     }
diff --git a/CTFd/themes/admin/static/js/teams/actions.js b/CTFd/themes/admin/static/js/teams/actions.js
index 2f93d0f9..6848b228 100644
--- a/CTFd/themes/admin/static/js/teams/actions.js
+++ b/CTFd/themes/admin/static/js/teams/actions.js
@@ -8,9 +8,12 @@ $(document).ready(function () {
             title: "Delete Team",
             body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(TEAM_NAME) + "</strong>"),
             success: function () {
-                var route = script_root + '/api/v1/teams/' + TEAM_ID;
-                $.delete(route, {}, function (data) {
-                    if (data.success) {
+                CTFd.fetch('/api/v1/teams/' + TEAM_ID, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
+                    if (response.success) {
                         window.location = script_root + '/admin/teams';
                     }
                 });
@@ -36,7 +39,7 @@ $(document).ready(function () {
             title: "Delete Submission",
             body: body,
             success: function () {
-                fetch(script_root + '/api/v1/submissions/' + submission_id, {
+                CTFd.fetch('/api/v1/submissions/' + submission_id, {
                     method: 'DELETE',
                     credentials: 'same-origin',
                     headers: {
@@ -70,7 +73,7 @@ $(document).ready(function () {
             title: "Delete Award",
             body: body,
             success: function () {
-                fetch(script_root + '/api/v1/awards/' + award_id, {
+                CTFd.fetch('/api/v1/awards/' + award_id, {
                     method: 'DELETE',
                     credentials: 'same-origin',
                     headers: {
diff --git a/CTFd/themes/admin/static/js/teams/info.js b/CTFd/themes/admin/static/js/teams/info.js
index 941c8ad4..6ad8b429 100644
--- a/CTFd/themes/admin/static/js/teams/info.js
+++ b/CTFd/themes/admin/static/js/teams/info.js
@@ -3,7 +3,7 @@ $(document).ready(function () {
         e.preventDefault();
         var params = $('#team-info-form').serializeJSON(true);
 
-        fetch(script_root + '/api/v1/teams/' + TEAM_ID, {
+        CTFd.fetch('/api/v1/teams/' + TEAM_ID, {
             method: 'PATCH',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/admin/static/js/teams/new.js b/CTFd/themes/admin/static/js/teams/new.js
index 64e9a14a..0de4b5cb 100644
--- a/CTFd/themes/admin/static/js/teams/new.js
+++ b/CTFd/themes/admin/static/js/teams/new.js
@@ -3,7 +3,7 @@ $(document).ready(function () {
         e.preventDefault();
         var params = $('#team-info-form').serializeJSON(true);
 
-        fetch(script_root + '/api/v1/teams', {
+        CTFd.fetch('/api/v1/teams', {
             method: 'POST',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/admin/static/js/users/actions.js b/CTFd/themes/admin/static/js/users/actions.js
index bf7b2efc..5b95b6e3 100644
--- a/CTFd/themes/admin/static/js/users/actions.js
+++ b/CTFd/themes/admin/static/js/users/actions.js
@@ -4,9 +4,12 @@ $(document).ready(function () {
             title: "Delete User",
             body: "Are you sure you want to delete {0}".format("<strong>" + htmlentities(USER_NAME) + "</strong>"),
             success: function () {
-                var route = script_root + '/api/v1/users/' + USER_ID;
-                $.delete(route, {}, function (data) {
-                    if (data.success) {
+                CTFd.fetch('/api/v1/users/' + USER_ID, {
+                    method: 'DELETE',
+                }).then(function (response) {
+                    return response.json();
+                }).then(function (response) {
+                    if (response.success) {
                         window.location = script_root + '/admin/users';
                     }
                 });
@@ -30,7 +33,7 @@ $(document).ready(function () {
         e.preventDefault();
         var params = $('#user-award-form').serializeJSON(true);
         params['user_id'] = USER_ID;
-        fetch(script_root + '/api/v1/awards', {
+        CTFd.fetch('/api/v1/awards', {
             method: 'POST',
             credentials: 'same-origin',
             headers: {
@@ -79,7 +82,7 @@ $(document).ready(function () {
             title: "Delete Submission",
             body: body,
             success: function () {
-                fetch(script_root + '/api/v1/submissions/' + submission_id, {
+                CTFd.fetch('/api/v1/submissions/' + submission_id, {
                     method: 'DELETE',
                     credentials: 'same-origin',
                     headers: {
@@ -113,7 +116,7 @@ $(document).ready(function () {
             title: "Delete Award",
             body: body,
             success: function () {
-                fetch(script_root + '/api/v1/awards/' + award_id, {
+                CTFd.fetch('/api/v1/awards/' + award_id, {
                     method: 'DELETE',
                     credentials: 'same-origin',
                     headers: {
@@ -153,7 +156,7 @@ $(document).ready(function () {
             title: "Mark Correct",
             body: body,
             success: function () {
-                fetch(script_root + '/api/v1/submissions', {
+                CTFd.fetch('/api/v1/submissions', {
                     method: 'POST',
                     credentials: 'same-origin',
                     headers: {
diff --git a/CTFd/themes/admin/static/js/users/info.js b/CTFd/themes/admin/static/js/users/info.js
index fa387368..a50d4db7 100644
--- a/CTFd/themes/admin/static/js/users/info.js
+++ b/CTFd/themes/admin/static/js/users/info.js
@@ -3,7 +3,7 @@ $(document).ready(function () {
         e.preventDefault();
         var params = $('#user-info-form').serializeJSON(true);
 
-        fetch(script_root + '/api/v1/users/' + USER_ID, {
+        CTFd.fetch('/api/v1/users/' + USER_ID, {
             method: 'PATCH',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/admin/static/js/users/new.js b/CTFd/themes/admin/static/js/users/new.js
index 3d86404a..6ca8d9c3 100644
--- a/CTFd/themes/admin/static/js/users/new.js
+++ b/CTFd/themes/admin/static/js/users/new.js
@@ -3,7 +3,7 @@ $(document).ready(function () {
         e.preventDefault();
         var params = $('#user-info-form').serializeJSON(true);
 
-        fetch(script_root + '/api/v1/users', {
+        CTFd.fetch('/api/v1/users', {
             method: 'POST',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/themes/core/static/js/CTFd.js b/CTFd/themes/core/static/js/CTFd.js
index 497c2f8f..b3daa60f 100644
--- a/CTFd/themes/core/static/js/CTFd.js
+++ b/CTFd/themes/core/static/js/CTFd.js
@@ -23,6 +23,10 @@ var CTFd = (function () {
         }
         url = this.options.urlRoot + url;
 
+
+        if (options.headers === undefined) {
+            options.headers = {};
+        }
         options.credentials = 'same-origin';
         options.headers['Accept'] = 'application/json';
         options.headers['Content-Type'] = 'application/json';
diff --git a/CTFd/themes/core/static/js/hints.js b/CTFd/themes/core/static/js/hints.js
index 1e561fd1..3e1b2d10 100644
--- a/CTFd/themes/core/static/js/hints.js
+++ b/CTFd/themes/core/static/js/hints.js
@@ -1,5 +1,5 @@
 function hint(id) {
-    return fetch(script_root + '/api/v1/hints/' + id, {
+    return CTFd.fetch('/api/v1/hints/' + id, {
         method: 'GET',
         credentials: 'same-origin',
         headers: {
@@ -13,7 +13,7 @@ function hint(id) {
 
 
 function unlock(params){
-    return fetch(script_root + '/api/v1/unlocks', {
+    return CTFd.fetch('/api/v1/unlocks', {
         method: 'POST',
         credentials: 'same-origin',
         headers: {
diff --git a/CTFd/themes/core/static/js/settings.js b/CTFd/themes/core/static/js/settings.js
index a50dc12f..d4d5c8a1 100644
--- a/CTFd/themes/core/static/js/settings.js
+++ b/CTFd/themes/core/static/js/settings.js
@@ -19,7 +19,7 @@ $(function () {
         $('#results').empty();
         var params = $('#user-settings-form').serializeJSON(true);
 
-        fetch(script_root + '/api/v1/users/me', {
+        CTFd.fetch('/api/v1/users/me', {
             method: 'PATCH',
             credentials: 'same-origin',
             headers: {
diff --git a/CTFd/utils/initialization/__init__.py b/CTFd/utils/initialization/__init__.py
index c9693bbe..240ad754 100644
--- a/CTFd/utils/initialization/__init__.py
+++ b/CTFd/utils/initialization/__init__.py
@@ -124,7 +124,10 @@ def init_request_processors(app):
             return
         if not session.get('nonce'):
             session['nonce'] = generate_nonce()
-        if request.method == "POST":
+        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
+            if request.content_type == 'application/json':
+                if session['nonce'] != request.headers.get('CSRF-Token'):
+                    abort(403)
             if request.content_type != 'application/json':
                 if session['nonce'] != request.form.get('nonce'):
                     abort(403)


Test patch:
diff --git a/tests/api/v1/test_csrf.py b/tests/api/v1/test_csrf.py
new file mode 100644
index 00000000..fcc2ef87
--- /dev/null
+++ b/tests/api/v1/test_csrf.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from flask.testing import FlaskClient
+from tests.helpers import *
+
+
+def test_api_csrf_failure():
+    """Can a user post /api/v1/awards if not admin"""
+    app = create_ctfd()
+    app.test_client_class = FlaskClient
+    with app.app_context():
+        with login_as_user(app, 'admin') as client:
+            r = client.post(
+                '/api/v1/challenges',
+                json={
+                    "name": "chal",
+                    "category": "cate",
+                    "description": "desc",
+                    "value": "100",
+                    "state": "hidden",
+                    "type": "standard"
+                }
+            )
+            assert r.status_code == 403
+
+            with client.session_transaction() as sess:
+                nonce = sess.get('nonce')
+
+            r = client.post(
+                '/api/v1/challenges',
+                headers={
+                    'CSRF-Token': nonce
+                },
+                json={
+                    "name": "chal",
+                    "category": "cate",
+                    "description": "desc",
+                    "value": "100",
+                    "state": "hidden",
+                    "type": "standard"
+                }
+            )
+            assert r.status_code == 200
+    destroy_ctfd(app)
diff --git a/tests/helpers.py b/tests/helpers.py
index 85e45041..40f2d18e 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -1,3 +1,5 @@
+from flask.testing import FlaskClient
+from werkzeug.datastructures import Headers
 from CTFd import create_app
 from CTFd.config import TestingConfig
 from CTFd.models import *
@@ -20,6 +22,19 @@ else:
 FakeRequest = namedtuple('FakeRequest', ['form'])
 
 
+class CTFdTestClient(FlaskClient):
+    def open(self, *args, **kwargs):
+        if kwargs.get('json') is not None:
+            with self.session_transaction() as sess:
+                api_key_headers = Headers({
+                    'CSRF-Token': sess.get('nonce')
+                })
+                headers = kwargs.pop('headers', Headers())
+                headers.extend(api_key_headers)
+                kwargs['headers'] = headers
+        return super(CTFdTestClient, self).open(*args, **kwargs)
+
+
 def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="password", user_mode="users", setup=True, enable_plugins=False, application_root='/'):
     if enable_plugins:
         TestingConfig.SAFE_MODE = False
@@ -29,6 +44,7 @@ def create_ctfd(ctf_name="CTFd", name="admin", email="admin@ctfd.io", password="
     TestingConfig.APPLICATION_ROOT = application_root
 
     app = create_app(TestingConfig)
+    app.test_client_class = CTFdTestClient
 
     if setup:
         app = setup_ctfd(app, ctf_name, name, email, password, user_mode)