Repository: CTFd/CTFd

Issue:
Flags are unable to specify why they failed
In the challenge plugin structure a given flag is unable to relay to the user why a given flag failed. 

A potential solution here is to raise an exception and then catch that from the challenge side and then echo out the string. 


Discussion:

Resolving patch:
diff --git a/CTFd/plugins/challenges/__init__.py b/CTFd/plugins/challenges/__init__.py
--- a/CTFd/plugins/challenges/__init__.py
+++ b/CTFd/plugins/challenges/__init__.py
@@ -11,7 +11,7 @@
     db,
 )
 from CTFd.plugins import register_plugin_assets_directory
-from CTFd.plugins.flags import get_flag_class
+from CTFd.plugins.flags import FlagException, get_flag_class
 from CTFd.utils.uploads import delete_file
 from CTFd.utils.user import get_ip
 
@@ -138,8 +138,11 @@ def attempt(challenge, request):
         submission = data["submission"].strip()
         flags = Flags.query.filter_by(challenge_id=challenge.id).all()
         for flag in flags:
-            if get_flag_class(flag.type).compare(flag, submission):
-                return True, "Correct"
+            try:
+                if get_flag_class(flag.type).compare(flag, submission):
+                    return True, "Correct"
+            except FlagException as e:
+                return False, e.message
         return False, "Incorrect"
 
     @staticmethod
diff --git a/CTFd/plugins/dynamic_challenges/__init__.py b/CTFd/plugins/dynamic_challenges/__init__.py
--- a/CTFd/plugins/dynamic_challenges/__init__.py
+++ b/CTFd/plugins/dynamic_challenges/__init__.py
@@ -16,7 +16,7 @@
 )
 from CTFd.plugins import register_plugin_assets_directory
 from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge
-from CTFd.plugins.flags import get_flag_class
+from CTFd.plugins.flags import FlagException, get_flag_class
 from CTFd.plugins.migrations import upgrade
 from CTFd.utils.modes import get_model
 from CTFd.utils.uploads import delete_file
@@ -184,8 +184,11 @@ def attempt(challenge, request):
         submission = data["submission"].strip()
         flags = Flags.query.filter_by(challenge_id=challenge.id).all()
         for flag in flags:
-            if get_flag_class(flag.type).compare(flag, submission):
-                return True, "Correct"
+            try:
+                if get_flag_class(flag.type).compare(flag, submission):
+                    return True, "Correct"
+            except FlagException as e:
+                return False, e.message
         return False, "Incorrect"
 
     @staticmethod
diff --git a/CTFd/plugins/flags/__init__.py b/CTFd/plugins/flags/__init__.py
--- a/CTFd/plugins/flags/__init__.py
+++ b/CTFd/plugins/flags/__init__.py
@@ -3,6 +3,14 @@
 from CTFd.plugins import register_plugin_assets_directory
 
 
+class FlagException(Exception):
+    def __init__(self, message):
+        self.message = message
+
+    def __str__(self):
+        return self.message
+
+
 class BaseFlag(object):
     name = None
     templates = {}
@@ -55,8 +63,8 @@ def compare(chal_key_obj, provided):
             else:
                 res = re.match(saved, provided)
         # TODO: this needs plugin improvements. See #1425.
-        except re.error:
-            return False
+        except re.error as e:
+            raise FlagException("Regex parse error occured") from e
 
         return res and res.group() == provided
 


Test patch:
diff --git a/tests/users/test_challenges.py b/tests/users/test_challenges.py
--- a/tests/users/test_challenges.py
+++ b/tests/users/test_challenges.py
@@ -181,6 +181,29 @@ def test_submitting_correct_regex_case_insensitive_flag():
     destroy_ctfd(app)
 
 
+def test_submitting_invalid_regex_flag():
+    """Test that invalid regex flags are errored out to the user"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        client = login_as_user(app)
+        chal = gen_challenge(app.db)
+        gen_flag(
+            app.db,
+            challenge_id=chal.id,
+            type="regex",
+            content="**",
+            data="case_insensitive",
+        )
+        data = {"submission": "FLAG", "challenge_id": chal.id}
+        r = client.post("/api/v1/challenges/attempt", json=data)
+        assert r.status_code == 200
+        resp = r.get_json()["data"]
+        assert resp.get("status") == "incorrect"
+        assert resp.get("message") == "Regex parse error occured"
+    destroy_ctfd(app)
+
+
 def test_submitting_incorrect_flag():
     """Test that incorrect flags are incorrect"""
     app = create_ctfd()