CVE-2022-4819: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: A vulnerability was found in HotCRP. It has been rated as problematic. Affected by this issue is some unknown functionality. The manipulation leads to cross site scripting. The attack may be launched remotely. The name of the patch is d4ffdb0ef806453c54ddca7fdda3e5c60356285c. It is recommended to apply a patch to fix this issue. VDB-216998 is the identifier assigned to this vulnerability.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-12-28T21:15Z
Repository: https://github.com/kohler/hotcrp
Language: PHP
Files changed: src/api/api_decision.php, src/assigners/a_decision.php, src/decisioninfo.php, src/help/h_keywords.php, src/listactions/la_decide.php, src/mailrecipients.php, src/papertable.php
Commit date: 2022-12-23 21:57:00 -0500
Commit message: Fix XSS (exploitable only by chairs) reported by Xiangyu Gao.

Diff: diff --git a/src/api/api_decision.php b/src/api/api_decision.php
index 193f3f79b..d9361be5c 100644
--- a/src/api/api_decision.php
+++ b/src/api/api_decision.php
@@ -16,11 +16,11 @@ class Decision_API {
                 return $aset->json_result();
             }
             $prow->load_decision();
         }
         $dec = $prow->viewable_decision($user);
-        $jr = new JsonResult(["ok" => true, "value" => $dec->id, "result" => htmlspecialchars($dec->name)]);
+        $jr = new JsonResult(["ok" => true, "value" => $dec->id, "result" => $dec->name_as(5)]);
         if ($user->can_set_decision($prow)) {
             $jr->content["editable"] = true;
         }
         return $jr;
     }
diff --git a/src/assigners/a_decision.php b/src/assigners/a_decision.php
index be030fe97..ccae8282b 100644
--- a/src/assigners/a_decision.php
+++ b/src/assigners/a_decision.php
@@ -107,11 +107,11 @@ class Decision_Assigner extends Assigner {
     }
     /** @param int $decid */
     static function decision_html(Conf $conf, $decid) {
         $dec = $conf->decision_set()->get($decid);
         $class = $dec->status_class();
-        $name_h = htmlspecialchars($dec->id === 0 ? "No decision" : $dec->name);
+        $name_h = $dec->id === 0 ? "No decision" : $dec->name_as(5);
         return "<span class=\"pstat {$class}\">{$name_h}</span>";
     }
     function unparse_display(AssignmentSet $aset) {
         $t = [];
         if ($this->item->existed()) {
diff --git a/src/decisioninfo.php b/src/decisioninfo.php
index 9abd88487..a6d40ae6d 100644
--- a/src/decisioninfo.php
+++ b/src/decisioninfo.php
@@ -50,10 +50,16 @@ class DecisionInfo {
         $dec = new DecisionInfo($id, "[#{$id}]");
         $dec->placeholder = true;
         return $dec;
     }
 
+    /** @param 0|5 $format
+     * @return string */
+    function name_as($format) {
+        return $format === 5 ? htmlspecialchars($this->name) : $this->name;
+    }
+
     /** @return string */
     function status_class() {
         return self::$class_name[$this->category] ?? "dec-maybe";
     }
 
diff --git a/src/help/h_keywords.php b/src/help/h_keywords.php
index 2b15290a6..c6c68070e 100644
--- a/src/help/h_keywords.php
+++ b/src/help/h_keywords.php
@@ -196,11 +196,11 @@ class Keywords_HelpTopic {
             if ($dec->id !== 0) {
                 $qdname = strtolower($dec->name);
                 if (strpos($qdname, " ") !== false) {
                     $qdname = "\"{$qdname}\"";
                 }
-                echo $hth->search_trow("dec:{$qdname}", "decision is “" . htmlspecialchars($dec->name) . "” (partial matches OK)");
+                echo $hth->search_trow("dec:{$qdname}", "decision is “" . $dec->name_as(5) . "” (partial matches OK)");
                 break;
             }
         }
         echo $hth->search_trow("dec:yes", "one of the accept decisions");
         echo $hth->search_trow("dec:no", "one of the reject decisions");
diff --git a/src/listactions/la_decide.php b/src/listactions/la_decide.php
index 8acdb0506..afa0c9baf 100644
--- a/src/listactions/la_decide.php
+++ b/src/listactions/la_decide.php
@@ -7,11 +7,11 @@ class Decide_ListAction extends ListAction {
         return $user->can_set_some_decision();
     }
     static function render(PaperList $pl, Qrequest $qreq) {
         $opts = [];
         foreach ($pl->conf->decision_set() as $dec) {
-            $opts[$dec->id] = $dec->name;
+            $opts[$dec->id] = $dec->name_as(5);
         }
         return ["Set to &nbsp;"
                 . Ht::select("decision", $opts, "", ["class" => "want-focus js-submit-action-info-decide"])
                 . $pl->action_submit("decide")];
     }
diff --git a/src/mailrecipients.php b/src/mailrecipients.php
index d49d4e522..400babdf9 100644
--- a/src/mailrecipients.php
+++ b/src/mailrecipients.php
@@ -145,11 +145,11 @@ class MailRecipients extends MessageSet {
             $this->dcounts();
             $this->defsel("bydec_group", "Contact authors by decision", self::F_GROUP);
             foreach ($this->conf->decision_set() as $dec) {
                 if ($dec->id !== 0) {
                     $hide = ($this->_dcounts[$dec->id] ?? 0) === 0;
-                    $this->defsel("dec:{$dec->name}", "Contact authors of " . htmlspecialchars($dec->name) . " papers", $hide ? self::F_HIDE : 0);
+                    $this->defsel("dec:{$dec->name}", "Contact authors of " . $dec->name_as(5) . " papers", $hide ? self::F_HIDE : 0);
                 }
             }
             $this->defsel("dec:yes", "Contact authors of accept-class papers", $this->_has_dt[2] ? 0 : self::F_HIDE);
             $this->defsel("dec:no", "Contact authors of reject-class papers", $this->_has_dt[0] ? 0 : self::F_HIDE);
             $this->defsel("dec:none", "Contact authors of undecided papers", $this->_has_dt[1] && ($this->_has_dt[0] || $this->_has_dt[2]) ? 0 : self::F_HIDE);
diff --git a/src/papertable.php b/src/papertable.php
index 54093365d..fafb53945 100644
--- a/src/papertable.php
+++ b/src/papertable.php
@@ -1545,17 +1545,17 @@ class PaperTable {
         if (isset($this->qreq->forceShow)) {
             echo Ht::hidden("forceShow", $this->qreq->forceShow ? 1 : 0);
         }
         $opts = [];
         foreach ($this->conf->decision_set() as $dec) {
-            $opts[$dec->id] = $dec->name;
+            $opts[$dec->id] = $dec->name_as(5);
         }
         echo Ht::select("decision", $opts,
                         (string) $this->prow->outcome,
                         ["class" => "w-99 want-focus", "id" => $id]),
             '</form><p class="fn odname js-psedit-result">',
-            htmlspecialchars($this->prow->decision()->name),
+            $this->prow->decision()->name_as(5),
             "</p></div>\n";
     }
 
     function papstripReviewPreference() {
         $this->_papstripBegin("revpref");

Vulnerable code:         $jr = new JsonResult(["ok" => true, "value" => $dec->id, "result" => htmlspecialchars($dec->name)]);
        $name_h = htmlspecialchars($dec->id === 0 ? "No decision" : $dec->name);
                echo $hth->search_trow("dec:{$qdname}", "decision is “" . htmlspecialchars($dec->name) . "” (partial matches OK)");
            $opts[$dec->id] = $dec->name;
                    $this->defsel("dec:{$dec->name}", "Contact authors of " . htmlspecialchars($dec->name) . " papers", $hide ? self::F_HIDE : 0);
            $opts[$dec->id] = $dec->name;
            htmlspecialchars($this->prow->decision()->name),
Fixed code:         $jr = new JsonResult(["ok" => true, "value" => $dec->id, "result" => $dec->name_as(5)]);
        $name_h = $dec->id === 0 ? "No decision" : $dec->name_as(5);
    /** @param 0|5 $format
     * @return string */
    function name_as($format) {
        return $format === 5 ? htmlspecialchars($this->name) : $this->name;
    }

                echo $hth->search_trow("dec:{$qdname}", "decision is “" . $dec->name_as(5) . "” (partial matches OK)");
            $opts[$dec->id] = $dec->name_as(5);
                    $this->defsel("dec:{$dec->name}", "Contact authors of " . $dec->name_as(5) . " papers", $hide ? self::F_HIDE : 0);
            $opts[$dec->id] = $dec->name_as(5);
            $this->prow->decision()->name_as(5),