CVE-2021-4315: Improper Control of Generation of Code ('Code Injection') (CWE-94)
value: A vulnerability has been found in NYUCCL psiTurk up to 3.2.0 and classified as critical. This vulnerability affects unknown code of the file psiturk/experiment.py. The manipulation of the argument mode leads to improper neutralization of special elements used in a template engine. The exploit has been disclosed to the public and may be used. Upgrading to version 3.2.1 is able to address this issue. The name of the patch is 47787e15cecd66f2aa87687bf852ae0194a4335f. It is recommended to upgrade the affected component. The identifier of this vulnerability is VDB-219676.
CWE description: The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
CVSS v3 base score: 8.8
Published: 2023-01-28T23:15Z
Repository: https://github.com/NYUCCL/psiTurk
Language: Python
Files changed: psiturk/experiment.py, tests/test_psiturk.py
Commit date: 2021-09-30 22:50:03 -0600
Commit message: Fix SSTI vulnerability in ad and consent pages (#517)

* Fix SSTI vulnerability in ad and consent pages

Fixed an issue where users could pass arbitrary Python code to be executed on the server to the mode HTTP arg

More information about this type of vulnerability: https://secure-cookie.io/attacks/ssti/
Diff: diff --git a/psiturk/experiment.py b/psiturk/experiment.py
index a6904f6..7ba1c1e 100644
--- a/psiturk/experiment.py
+++ b/psiturk/experiment.py
@@ -378,13 +378,14 @@ def advertisement():
     elif status == ALLOCATED or not status or debug_mode:
         # Participant has not yet agreed to the consent. They might not
         # even have accepted the HIT.
         with open('templates/ad.html', 'r') as temp_file:
             ad_string = temp_file.read()
-        ad_string = insert_mode(ad_string, mode)
+        ad_string = insert_mode(ad_string)
         return render_template_string(
             ad_string,
+            mode=mode,
             hitid=hit_id,
             assignmentid=assignment_id,
             workerid=worker_id
         )
     else:
@@ -404,13 +405,14 @@ def give_consent():
     assignment_id = request.args['assignmentId']
     worker_id = request.args['workerId']
     mode = request.args['mode']
     with open('templates/consent.html', 'r') as temp_file:
         consent_string = temp_file.read()
-    consent_string = insert_mode(consent_string, mode)
+    consent_string = insert_mode(consent_string)
     return render_template_string(
         consent_string,
+        mode=mode,
         hitid=hit_id,
         assignmentid=assignment_id,
         workerid=worker_id
     )
 
@@ -729,20 +731,20 @@ def ppid():
 
 # Insert "mode" into pages so it's carried from page to page done server-side
 # to avoid breaking backwards compatibility with old templates.
 
 
-def insert_mode(page_html, mode):
+def insert_mode(page_html):
     """ Insert mode """
     page_html = page_html
     match_found = False
     matches = re.finditer('workerId={{ workerid }}', page_html)
     match = None
     for match in matches:
         match_found = True
     if match_found:
-        new_html = page_html[:match.end()] + "&mode=" + mode +\
+        new_html = page_html[:match.end()] + '&mode={{ mode }}' +\
             page_html[match.end():]
         return new_html
     else:
         raise ExperimentError("insert_mode_failed")
 
diff --git a/tests/test_psiturk.py b/tests/test_psiturk.py
index 497f0a5..5229962 100644
--- a/tests/test_psiturk.py
+++ b/tests/test_psiturk.py
@@ -139,11 +139,11 @@ def test_does_not_die_if_no_custompy(remove_file, psiturk_test_client):
 def test_insert_mode(psiturk_test_client):
     with open('templates/ad.html', 'r') as temp_file:
         ad_string = temp_file.read()
 
     from psiturk.experiment import insert_mode
-    insert_mode(ad_string, 'debug')
+    insert_mode(ad_string)
 
 
 class PsiTurkStandardTests(PsiturkUnitTest):
 
     # Test experiment.py

Vulnerable code:         ad_string = insert_mode(ad_string, mode)
    consent_string = insert_mode(consent_string, mode)
def insert_mode(page_html, mode):
        new_html = page_html[:match.end()] + "&mode=" + mode +\
    insert_mode(ad_string, 'debug')
Fixed code:         ad_string = insert_mode(ad_string)
            mode=mode,
    consent_string = insert_mode(consent_string)
        mode=mode,
def insert_mode(page_html):
        new_html = page_html[:match.end()] + '&mode={{ mode }}' +\
    insert_mode(ad_string)