mirror of
https://github.com/aljazceru/CTFd.git
synced 2025-12-17 14:04:20 +01:00
Fix frontend UI where empty/null requirements could be added (#1824)
* Fix Challenge Requirements interface in Admin Panel to not allow empty/null requirements to be added * Closes #1809
This commit is contained in:
@@ -1,9 +1,29 @@
|
||||
from marshmallow import validate
|
||||
from marshmallow.exceptions import ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
|
||||
from CTFd.models import Challenges, ma
|
||||
|
||||
|
||||
class ChallengeRequirementsValidator(validate.Validator):
|
||||
default_message = "Error parsing challenge requirements"
|
||||
|
||||
def __init__(self, error=None):
|
||||
self.error = error or self.default_message
|
||||
|
||||
def __call__(self, value):
|
||||
if isinstance(value, dict) is False:
|
||||
raise ValidationError(self.default_message)
|
||||
|
||||
prereqs = value.get("prerequisites", [])
|
||||
if all(prereqs) is False:
|
||||
raise ValidationError(
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ChallengeSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Challenges
|
||||
@@ -46,3 +66,7 @@ class ChallengeSchema(ma.ModelSchema):
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
requirements = field_for(
|
||||
Challenges, "requirements", validate=[ChallengeRequirementsValidator()],
|
||||
)
|
||||
|
||||
@@ -43,7 +43,12 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-success float-right">Add Prerequisite</button>
|
||||
<button
|
||||
class="btn btn-success float-right"
|
||||
:disabled="!selectedRequirement"
|
||||
>
|
||||
Add Prerequisite
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -123,6 +128,10 @@ export default {
|
||||
? this.requirements.prerequisites
|
||||
: [];
|
||||
|
||||
if (!this.selectedRequirement) {
|
||||
return;
|
||||
}
|
||||
|
||||
newRequirements.push(this.selectedRequirement);
|
||||
this.requirements["prerequisites"] = newRequirements;
|
||||
|
||||
@@ -144,6 +153,7 @@ export default {
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.selectedRequirement = null;
|
||||
this.loadRequirements();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -668,7 +668,7 @@ eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
;
|
||||
eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar _default = {\n props: {\n challenge_id: Number\n },\n data: function data() {\n return {\n challenges: [],\n requirements: {},\n selectedRequirement: null\n };\n },\n computed: {\n // Get all challenges besides the current one and current prereqs\n otherChallenges: function otherChallenges() {\n var _this = this;\n\n var prerequisites = this.requirements.prerequisites || [];\n return this.challenges.filter(function (challenge) {\n return challenge.id !== _this.$props.challenge_id && !prerequisites.includes(challenge.id);\n });\n }\n },\n methods: {\n loadChallenges: function loadChallenges() {\n var _this2 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges?view=admin\", {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n _this2.challenges = response.data;\n }\n });\n },\n getChallengeById: function getChallengeById(challenge_id) {\n return this.challenges.find(function (challenge) {\n return challenge.id === challenge_id;\n });\n },\n loadRequirements: function loadRequirements() {\n var _this3 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id, \"/requirements\"), {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n _this3.requirements = response.data || {};\n }\n });\n },\n addRequirement: function addRequirement() {\n var _this4 = this;\n\n var newRequirements = this.requirements.prerequisites ? this.requirements.prerequisites : [];\n newRequirements.push(this.selectedRequirement);\n this.requirements[\"prerequisites\"] = newRequirements;\n var params = {\n requirements: this.requirements\n };\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id), {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (data) {\n if (data.success) {\n _this4.loadRequirements();\n }\n });\n },\n removeRequirement: function removeRequirement(challenge_id) {\n var _this5 = this;\n\n this.requirements.prerequisites = this.requirements.prerequisites.filter(function (val) {\n return val !== challenge_id;\n });\n var params = {\n requirements: this.requirements\n };\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id), {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (data) {\n if (data.success) {\n _this5.loadRequirements();\n }\n });\n }\n },\n created: function created() {\n this.loadChallenges();\n this.loadRequirements();\n }\n};\nexports[\"default\"] = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/requirements/Requirements.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options");
|
||||
eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _CTFd = _interopRequireDefault(__webpack_require__(/*! core/CTFd */ \"./CTFd/themes/core/assets/js/CTFd.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\nvar _default = {\n props: {\n challenge_id: Number\n },\n data: function data() {\n return {\n challenges: [],\n requirements: {},\n selectedRequirement: null\n };\n },\n computed: {\n // Get all challenges besides the current one and current prereqs\n otherChallenges: function otherChallenges() {\n var _this = this;\n\n var prerequisites = this.requirements.prerequisites || [];\n return this.challenges.filter(function (challenge) {\n return challenge.id !== _this.$props.challenge_id && !prerequisites.includes(challenge.id);\n });\n }\n },\n methods: {\n loadChallenges: function loadChallenges() {\n var _this2 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges?view=admin\", {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n _this2.challenges = response.data;\n }\n });\n },\n getChallengeById: function getChallengeById(challenge_id) {\n return this.challenges.find(function (challenge) {\n return challenge.id === challenge_id;\n });\n },\n loadRequirements: function loadRequirements() {\n var _this3 = this;\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id, \"/requirements\"), {\n method: \"GET\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n }\n }).then(function (response) {\n return response.json();\n }).then(function (response) {\n if (response.success) {\n _this3.requirements = response.data || {};\n }\n });\n },\n addRequirement: function addRequirement() {\n var _this4 = this;\n\n var newRequirements = this.requirements.prerequisites ? this.requirements.prerequisites : [];\n\n if (!this.selectedRequirement) {\n return;\n }\n\n newRequirements.push(this.selectedRequirement);\n this.requirements[\"prerequisites\"] = newRequirements;\n var params = {\n requirements: this.requirements\n };\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id), {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (data) {\n if (data.success) {\n _this4.selectedRequirement = null;\n\n _this4.loadRequirements();\n }\n });\n },\n removeRequirement: function removeRequirement(challenge_id) {\n var _this5 = this;\n\n this.requirements.prerequisites = this.requirements.prerequisites.filter(function (val) {\n return val !== challenge_id;\n });\n var params = {\n requirements: this.requirements\n };\n\n _CTFd[\"default\"].fetch(\"/api/v1/challenges/\".concat(this.$props.challenge_id), {\n method: \"PATCH\",\n credentials: \"same-origin\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify(params)\n }).then(function (response) {\n return response.json();\n }).then(function (data) {\n if (data.success) {\n _this5.loadRequirements();\n }\n });\n }\n },\n created: function created() {\n this.loadChallenges();\n this.loadRequirements();\n }\n};\nexports[\"default\"] = _default;\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/requirements/Requirements.vue?./node_modules/babel-loader/lib??ref--0!./node_modules/vue-loader/lib??vue-loader-options");
|
||||
|
||||
/***/ }),
|
||||
|
||||
@@ -847,7 +847,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) *
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
;
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", [\n _c(\"table\", { staticClass: \"table table-striped text-center\" }, [\n _vm._m(0),\n _vm._v(\" \"),\n _c(\n \"tbody\",\n { attrs: { id: \"challenge-solves-body\" } },\n _vm._l(_vm.requirements.prerequisites, function(requirement) {\n return _c(\"tr\", { key: requirement }, [\n _c(\"td\", [_vm._v(_vm._s(_vm.getChallengeById(requirement).name))]),\n _vm._v(\" \"),\n _c(\"td\", [\n _c(\"i\", {\n staticClass: \"btn-fa fas fa-times delete-requirement\",\n attrs: { role: \"button\", \"challenge-id\": requirement },\n on: {\n click: function($event) {\n return _vm.removeRequirement(requirement)\n }\n }\n })\n ])\n ])\n }),\n 0\n )\n ]),\n _vm._v(\" \"),\n _c(\n \"form\",\n {\n on: {\n submit: function($event) {\n $event.preventDefault()\n return _vm.addRequirement($event)\n }\n }\n },\n [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\n \"select\",\n {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model\",\n value: _vm.selectedRequirement,\n expression: \"selectedRequirement\"\n }\n ],\n staticClass: \"form-control custom-select\",\n attrs: { name: \"prerequisite\" },\n on: {\n change: function($event) {\n var $$selectedVal = Array.prototype.filter\n .call($event.target.options, function(o) {\n return o.selected\n })\n .map(function(o) {\n var val = \"_value\" in o ? o._value : o.value\n return val\n })\n _vm.selectedRequirement = $event.target.multiple\n ? $$selectedVal\n : $$selectedVal[0]\n }\n }\n },\n [\n _c(\"option\", { attrs: { value: \"\" } }, [_vm._v(\" -- \")]),\n _vm._v(\" \"),\n _vm._l(_vm.otherChallenges, function(challenge) {\n return _c(\n \"option\",\n { key: challenge.id, domProps: { value: challenge.id } },\n [\n _vm._v(\n \"\\n \" + _vm._s(challenge.name) + \"\\n \"\n )\n ]\n )\n })\n ],\n 2\n )\n ]),\n _vm._v(\" \"),\n _vm._m(1)\n ]\n )\n ])\n}\nvar staticRenderFns = [\n function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"thead\", [\n _c(\"tr\", [\n _c(\"td\", [_c(\"b\", [_vm._v(\"Requirement\")])]),\n _vm._v(\" \"),\n _c(\"td\", [_c(\"b\", [_vm._v(\"Settings\")])])\n ])\n ])\n },\n function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\"button\", { staticClass: \"btn btn-success float-right\" }, [\n _vm._v(\"Add Prerequisite\")\n ])\n ])\n }\n]\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/requirements/Requirements.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
|
||||
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"render\", function() { return render; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"staticRenderFns\", function() { return staticRenderFns; });\nvar render = function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"div\", [\n _c(\"table\", { staticClass: \"table table-striped text-center\" }, [\n _vm._m(0),\n _vm._v(\" \"),\n _c(\n \"tbody\",\n { attrs: { id: \"challenge-solves-body\" } },\n _vm._l(_vm.requirements.prerequisites, function(requirement) {\n return _c(\"tr\", { key: requirement }, [\n _c(\"td\", [_vm._v(_vm._s(_vm.getChallengeById(requirement).name))]),\n _vm._v(\" \"),\n _c(\"td\", [\n _c(\"i\", {\n staticClass: \"btn-fa fas fa-times delete-requirement\",\n attrs: { role: \"button\", \"challenge-id\": requirement },\n on: {\n click: function($event) {\n return _vm.removeRequirement(requirement)\n }\n }\n })\n ])\n ])\n }),\n 0\n )\n ]),\n _vm._v(\" \"),\n _c(\n \"form\",\n {\n on: {\n submit: function($event) {\n $event.preventDefault()\n return _vm.addRequirement($event)\n }\n }\n },\n [\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\n \"select\",\n {\n directives: [\n {\n name: \"model\",\n rawName: \"v-model\",\n value: _vm.selectedRequirement,\n expression: \"selectedRequirement\"\n }\n ],\n staticClass: \"form-control custom-select\",\n attrs: { name: \"prerequisite\" },\n on: {\n change: function($event) {\n var $$selectedVal = Array.prototype.filter\n .call($event.target.options, function(o) {\n return o.selected\n })\n .map(function(o) {\n var val = \"_value\" in o ? o._value : o.value\n return val\n })\n _vm.selectedRequirement = $event.target.multiple\n ? $$selectedVal\n : $$selectedVal[0]\n }\n }\n },\n [\n _c(\"option\", { attrs: { value: \"\" } }, [_vm._v(\" -- \")]),\n _vm._v(\" \"),\n _vm._l(_vm.otherChallenges, function(challenge) {\n return _c(\n \"option\",\n { key: challenge.id, domProps: { value: challenge.id } },\n [\n _vm._v(\n \"\\n \" + _vm._s(challenge.name) + \"\\n \"\n )\n ]\n )\n })\n ],\n 2\n )\n ]),\n _vm._v(\" \"),\n _c(\"div\", { staticClass: \"form-group\" }, [\n _c(\n \"button\",\n {\n staticClass: \"btn btn-success float-right\",\n attrs: { disabled: !_vm.selectedRequirement }\n },\n [_vm._v(\"\\n Add Prerequisite\\n \")]\n )\n ])\n ]\n )\n ])\n}\nvar staticRenderFns = [\n function() {\n var _vm = this\n var _h = _vm.$createElement\n var _c = _vm._self._c || _h\n return _c(\"thead\", [\n _c(\"tr\", [\n _c(\"td\", [_c(\"b\", [_vm._v(\"Requirement\")])]),\n _vm._v(\" \"),\n _c(\"td\", [_c(\"b\", [_vm._v(\"Settings\")])])\n ])\n ])\n }\n]\nrender._withStripped = true\n\n\n\n//# sourceURL=webpack:///./CTFd/themes/admin/assets/js/components/requirements/Requirements.vue?./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options");
|
||||
|
||||
/***/ }),
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -397,6 +397,47 @@ def test_hidden_challenge_is_unsolveable():
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_invalid_requirements_are_rejected():
|
||||
"""Test that invalid requirements JSON blobs are rejected by the API"""
|
||||
app = create_ctfd()
|
||||
with app.app_context():
|
||||
gen_challenge(app.db)
|
||||
gen_challenge(app.db)
|
||||
with login_as_user(app, "admin") as client:
|
||||
# Test None/null values
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [None]}}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"requirements": [
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
]
|
||||
},
|
||||
}
|
||||
# Test empty strings
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [""]}}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.get_json() == {
|
||||
"success": False,
|
||||
"errors": {
|
||||
"requirements": [
|
||||
"Challenge requirements cannot have a null prerequisite"
|
||||
]
|
||||
},
|
||||
}
|
||||
# Test a valid integer
|
||||
r = client.patch(
|
||||
"/api/v1/challenges/1", json={"requirements": {"prerequisites": [2]}}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
destroy_ctfd(app)
|
||||
|
||||
|
||||
def test_challenge_with_requirements_is_unsolveable():
|
||||
"""Test that a challenge with a requirement is unsolveable without first solving the requirement"""
|
||||
app = create_ctfd()
|
||||
|
||||
Reference in New Issue
Block a user