Skip to content

Commit

Permalink
Allow hints and flags to contain JSON to allow for better data control (
Browse files Browse the repository at this point in the history
CTFd#2460)

Allow challenge CSV to contain JSON in the hints and flags columns so that users can insert more complex data that includes commas. 

* Closes CTFd#2038 
* Closes CTFd#2395
  • Loading branch information
ColdHeat authored Jan 27, 2024
1 parent ef6752c commit 39cf659
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 20 deletions.
65 changes: 48 additions & 17 deletions CTFd/utils/csv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,15 +364,32 @@ def load_challenges_csv(dict_reader):
db.session.commit()

if flags:
flags = [flag.strip() for flag in flags.split(",")]
for flag in flags:
f = Flags(
type="static",
challenge_id=challenge.id,
content=flag,
)
db.session.add(f)
db.session.commit()
try:
# Allow for column to contain JSON for more flexible data entry
flags = json.loads(flags)
for flag in flags:
type = flag.get("type", "static")
content = flag.get("content", "")
data = flag.get("data", None)
f = Flags(
challenge_id=challenge.id,
type=type,
content=content,
data=data,
)
db.session.add(f)
db.session.commit()

except json.JSONDecodeError:
flags = [flag.strip() for flag in flags.split(",")]
for flag in flags:
f = Flags(
type="static",
challenge_id=challenge.id,
content=flag,
)
db.session.add(f)
db.session.commit()

if tags:
tags = [tag.strip() for tag in tags.split(",")]
Expand All @@ -385,14 +402,28 @@ def load_challenges_csv(dict_reader):
db.session.commit()

if hints:
hints = [hint.strip() for hint in hints.split(",")]
for hint in hints:
h = Hints(
challenge_id=challenge.id,
content=hint,
)
db.session.add(h)
db.session.commit()
try:
# Allow for column to contain JSON for more flexible data entry
hints = json.loads(hints)
for hint in hints:
content = hint.get("content", "")
cost = hint.get("cost", 0)
h = Hints(
challenge_id=challenge.id,
content=content,
cost=cost,
)
db.session.add(h)
db.session.commit()
except json.JSONDecodeError:
hints = [hint.strip() for hint in hints.split(",")]
for hint in hints:
h = Hints(
challenge_id=challenge.id,
content=hint,
)
db.session.add(h)
db.session.commit()
if errors:
return errors
return True
Expand Down
56 changes: 53 additions & 3 deletions tests/admin/test_csv.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import io

from CTFd.models import Challenges, Teams, Users
from CTFd.models import Challenges, Flags, Hints, Teams, Users
from CTFd.utils.crypto import verify_password
from tests.helpers import create_ctfd, destroy_ctfd, gen_challenge, login_as_user

Expand Down Expand Up @@ -30,8 +30,8 @@ def test_import_csv_works():
team1,team1@examplectf.com,password
team2,team2@examplectf.com,password"""

CHALLENGES_CSV = b"""name,category,description,value,flags,tags,hints
challenge1,category1,description1,100,"flag1,flag2,flag3","tag1,tag2,tag3","hint1,hint2,hint3"""
CHALLENGES_CSV = b'''name,category,description,value,flags,tags,hints
challenge1,category1,description1,100,"flag1,flag2,flag3","tag1,tag2,tag3","hint1,hint2,hint3"'''

app = create_ctfd()
with app.app_context():
Expand Down Expand Up @@ -91,3 +91,53 @@ def test_import_csv_works():
assert len(challenge.hints) == 3

destroy_ctfd(app)


def test_import_challenge_csv_with_json():

CHALLENGES_CSV = b'''name,category,description,value,flags,tags,hints
challenge1,category1,description1,100,"[{""type"": ""static"", ""content"": ""flag1"", ""data"": ""case_insensitive""}, {""type"": ""regex"", ""content"": ""(.*)"", ""data"": ""case_insensitive""}, {""type"": ""static"", ""content"": ""flag3""}]","tag1,tag2,tag3","[{""content"": ""hint1"", ""cost"": 10}, {""content"": ""hint2"", ""cost"": 20}, {""content"": ""hint3"", ""cost"": 30}]"'''

app = create_ctfd()
with app.app_context():
client = login_as_user(app, name="admin", password="password")

with client.session_transaction() as sess:
data = {
"csv_type": "challenges",
"csv_file": (io.BytesIO(CHALLENGES_CSV), "challenges.csv"),
"nonce": sess.get("nonce"),
}

client.post("/admin/import/csv", data=data, content_type="multipart/form-data")
assert Challenges.query.count() == 1
challenge = Challenges.query.filter_by(id=1).first()
assert challenge.name == "challenge1"
assert challenge.category == "category1"
assert challenge.description == "description1"
assert challenge.value == 100
assert len(challenge.flags) == 3
assert len(challenge.tags) == 3
assert len(challenge.hints) == 3

for i in range(1, 4):
h = Hints.query.filter_by(id=i).first()
assert h.cost == i * 10
assert h.content == f"hint{i}"

f = Flags.query.filter_by(id=1).first()
assert f.type == "static"
assert f.content == "flag1"
assert f.data == "case_insensitive"

f = Flags.query.filter_by(id=2).first()
assert f.type == "regex"
assert f.content == "(.*)"
assert f.data == "case_insensitive"

f = Flags.query.filter_by(id=3).first()
assert f.type == "static"
assert f.content == "flag3"
assert f.data is None

destroy_ctfd(app)

0 comments on commit 39cf659

Please sign in to comment.