CVE-2022-4590: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: A vulnerability was found in mschaef toto up to 1.4.20. It has been classified as problematic. This affects an unknown part of the component Todo List Handler. The manipulation leads to cross site scripting. It is possible to initiate the attack remotely. Upgrading to version 1.4.21 is able to address this issue. The name of the patch is fdc825ac5249f40683377e8a526a06cdc6870125. It is recommended to upgrade the affected component. The identifier VDB-216177 was 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-17T19:15Z
Repository: https://github.com/mschaef/toto
Language: Other
Files changed: src/toto/todo/todo_list.clj, src/toto/todo/todo_list_manager.clj, src/toto/view/page.clj
Commit date: 2022-10-28 06:27:03 -0400
Commit message: Escape user entered HTML in several different locations throughout the UI, to help mitigate against XSS attacks.

Diff: diff --git a/src/toto/todo/todo_list.clj b/src/toto/todo/todo_list.clj
index eaaeffe..c73d58d 100644
--- a/src/toto/todo/todo_list.clj
+++ b/src/toto/todo/todo_list.clj
@@ -26,12 +26,12 @@
         toto.view.icons
         toto.view.components
         toto.view.query
         toto.view.page)
   (:require [clojure.tools.logging :as log]
-            [hiccup.form :as form]
-            [hiccup.util :as util]
+            [hiccup.form :as hiccup-form]
+            [hiccup.util :as hiccup-util]
             [toto.data.data :as data]
             [toto.view.auth :as auth]
             [toto.view.request-date :as request-date]
             [toto.todo.sidebar-view :as sidebar-view]))
 
@@ -40,23 +40,23 @@
 (def pill-date-format (java.text.SimpleDateFormat. "yyyy-MM-dd"))
 
 (defn- format-date [ date ]
   (.format pill-date-format date))
 
-(defn ensure-string-breakpoints [ s n ]
-  (clojure.string/join html-breakpoint (partition-string s n)))
+(defn- ensure-string-breakpoints [ s n ]
+  (clojure.string/join html-breakpoint (map hiccup-util/escape-html (partition-string s n))))
 
 (defn- ensure-string-breaks [ string at ]
   (clojure.string/replace string at (str at html-breakpoint)))
 
 (defn shorten-url-text [ url-text target-length ]
   (let [url (java.net.URL. url-text)
         base (str (.getProtocol url)
                   ":"
                   (if-let [authority (.getAuthority url)]
                     (str "//" authority)))]
-    (-> (util/escape-html
+    (-> (hiccup-util/escape-html
          (str base
               (string-leftmost (.getPath url)
                                (max 0 (- (- target-length 3) (.length base)))
                                "...")))
         (ensure-string-breaks "/")
@@ -97,26 +97,27 @@
       (item-priority-button item-id 1 img-star-gray writable?)
       (item-priority-button item-id 0 img-star-yellow writable?))))
 
 (defn- render-new-item-form [ list-id editing-item? ]
   (let [ sublists (data/get-view-sublists (auth/current-user-id) list-id)]
-    (form/form-to
+    (hiccup-form/form-to
      {:class "new-item-form"}
      [:post (shref "/list/" list-id)]
      (if (= (count sublists) 0)
-       (form/hidden-field "item-list-id" list-id)
+       (hiccup-form/hidden-field "item-list-id" list-id)
        [:select {:id "item-list-id" :name "item-list-id"}
-        (form/select-options (map (fn [ sublist ]
-                                    [ (:desc sublist) (:sublist_id sublist)])
+        (hiccup-form/select-options (map (fn [ sublist ]
+                                           [(hiccup-util/escape-html (:desc sublist))
+                                            (:sublist_id sublist)])
                                   sublists))])
-     (form/text-field (cond-> {:maxlength "1024"
+     (hiccup-form/text-field (cond-> {:maxlength "1024"
                                :placeholder "New Item Description"
                                :autocomplete "off"
                                :onkeydown "window._toto.onNewItemInputKeydown(event)"}
                         (not editing-item?) (assoc "autofocus" "on"))
                       "item-description")
-     (form/hidden-field "item-priority" "0")
+     (hiccup-form/hidden-field "item-priority" "0")
      [:button.high-priority-submit {:type "button"
                                     :onclick "window._toto.submitHighPriority()"}
       img-star-yellow])))
 
 (def url-regex #"(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]")
@@ -198,29 +199,30 @@
                                           (when currently-snoozed
                                             (list
                                              ", snoozed: " (format-date snoozed-until)))])
            (when (not (= created-by-id (auth/current-user-id)))
              [:span.pill { :title created-by-email }
-              created-by-name])]))]
+              (hiccup-util/escape-html
+               created-by-name)])]))]
      [:div.item-control.priority.right
       (render-item-priority-control item-id priority writable?)]
      (item-drag-handle "right" item-info)]))
 
 (defn- render-query-select [ id current-value ]
   [:select { :id id :name id :onchange "this.form.submit()"}
-   (form/select-options [[ "-" "-"]
+   (hiccup-form/select-options [[ "-" "-"]
                          ["1d" "1"]
                          ["7d" "7"]
                          ["30d" "30"]
                          ["90d" "90"] ]
                         (if (nil? current-value)
                           "-"
                           (str current-value)))])
 
 (defn- render-todo-list-query-settings [ list-id completed-within-days snoozed-for-days ]
   [:div.query-settings
-   (form/form-to { :class "embedded "} [:get (shref "/list/" list-id)]
+   (hiccup-form/form-to { :class "embedded "} [:get (shref "/list/" list-id)]
                  [:div.control-segment
                   [:a {:href (shref "/list/" list-id {:view "completions"})}
                    "[recently completed]"]]
                  [:div.control-segment
                   [:a {:href (shref "/list/" list-id "/details")}
@@ -235,11 +237,11 @@
                  [:div.control-segment
                   [:a { :href (shref "/list/" list-id {:modal "update-from"} ) } "[copy from]"]])])
 
 (defn- render-todo-list-completion-query-settings [ list-id completed-within-days ]
   [:div.query-settings
-   (form/form-to { :class "embedded "} [:get (shref "/list/" list-id "/completions")]
+   (hiccup-form/form-to { :class "embedded "} [:get (shref "/list/" list-id "/completions")]
                  [:div.control-segment
                   [:a {:href (shref "/list/" list-id "/details")}
                    "[list details]"]]
                  [:div.control-segment
                   [:a {:href (shref (str list-id) {:view :remove})}
@@ -304,11 +306,12 @@
     (when (> (count items) 0)
       [:div.list-view-section
        [:h2
         [:a
          {:href (shref "/list/" (:sublist_id sublist-details))}
-         (:desc sublist-details)]]
+         (hiccup-util/escape-html
+          (:desc sublist-details))]]
        items])))
 
 (defn- render-todo-list-view [ list-id edit-item-id writable? completed-within-days snoozed-for-days ]
   (let [ sublists (map #(merge % (todo-list-details list-id (:sublist_id %) edit-item-id writable? completed-within-days snoozed-for-days))
                        (data/get-view-sublists (auth/current-user-id) list-id))]
@@ -431,15 +434,17 @@
         [:hr]
         (render-snooze-choice "Unsnooze" 0 "0")]))))
 
 (defn- render-list-select [ id excluded-list-id ]
   [:select { :id id :name id }
-   (form/select-options (map (fn [ list-info ]
-                               [ (:desc list-info) (:todo_list_id list-info)])
-                             (remove
-                              #(= excluded-list-id (:todo_list_id %))
-                              (data/get-todo-lists-by-user (auth/current-user-id)))))])
+   (hiccup-form/select-options
+    (map (fn [ list-info ]
+           [(hiccup-util/escape-html (:desc list-info))
+            (:todo_list_id list-info)])
+         (remove
+          #(= excluded-list-id (:todo_list_id %))
+          (data/get-todo-lists-by-user (auth/current-user-id)))))])
 
 (defn render-update-from-modal [ params list-id ]
   (render-modal
    {:title "Update From"
     :form-post-to (shref "/list/" list-id "/copy-from" without-modal)}
diff --git a/src/toto/todo/todo_list_manager.clj b/src/toto/todo/todo_list_manager.clj
index 201366b..8bb5e80 100644
--- a/src/toto/todo/todo_list_manager.clj
+++ b/src/toto/todo/todo_list_manager.clj
@@ -26,11 +26,12 @@
         toto.view.icons
         toto.view.components
         toto.view.query
         toto.view.page)
   (:require [clojure.tools.logging :as log]
-            [hiccup.form :as form]
+            [hiccup.form :as hiccup-form]
+            [hiccup.util :as hiccup-util]
             [toto.data.data :as data]
             [toto.view.auth :as auth]
             [toto.todo.sidebar-view :as sidebar-view]))
 
 (defn- list-priority-button [ list-id new-priority image-spec ]
@@ -48,19 +49,19 @@
   (if (>= priority 0)
     (list-priority-button list-id -1 img-arrow-gray)
     (list-priority-button list-id 0 img-arrow-blue)))
 
 (defn render-new-list-form [ ]
-  (form/form-to
+  (hiccup-form/form-to
    {:class "new-item-form"}
    [:post (shref "/list")]
-   (form/text-field {:maxlength "32"
+   (hiccup-form/text-field {:maxlength "32"
                      :placeholder "New List Name"
                      :autofocus "autofocus"}
                     "list-description")
    [:div
-    (form/check-box "is-view" false "Y")
+    (hiccup-form/check-box "is-view" false "Y")
     [:label {:for "is-view"} "View"]]))
 
 (defn render-list-list-page []
   (render-page
    {:title "Manage Todo Lists"}
@@ -87,14 +88,14 @@
           (data/get-todo-lists-by-user (auth/current-user-id)))])))
 
 (defn- render-sort-list-panel [ list-id ]
   [:div.config-panel
    [:h1 "Sort List"]
-   (form/form-to {} [:post (shref "/list/" list-id "/sort")]
+   (hiccup-form/form-to {} [:post (shref "/list/" list-id "/sort")]
     [:input {:type "submit" :value "Sort By"}]
     [:select {:id "sort-by" :name "sort-by"}
-     (form/select-options [["Description" "desc"]
+     (hiccup-form/select-options [["Description" "desc"]
                            ["Created Date" "created-on"]
                            ["Updated Date" "updated-on"]
                            ["Snoozed Until" "snoozed-until"]])])])
 
 (defn- render-list-delete-panel [ list-id ]
@@ -117,11 +118,11 @@
   (let [list-details (data/get-todo-list-by-id list-id)]
     (list
      [:div.config-panel
       [:h1  "List Permissions:"]
       [:div
-       (form/check-box "is-public" (:is_public list-details))
+       (hiccup-form/check-box "is-public" (:is_public list-details))
        [:label {:for "is-public"} "List publically visible?"]]]
      [:div.config-panel
       [:h1  "List Owners:"]
       (let [list-owners (data/get-todo-list-owners-by-list-id list-id) ]
         [:div.list-owners
@@ -129,12 +130,12 @@
                 (let [ user-parameter-name (str "user_" user-id)]
                   [:div.list-owner
                    (if (= (auth/current-user-id) user-id)
                      [:div.self-owner
                       "&nbsp;"
-                      (form/hidden-field user-parameter-name "on")]
-                     (form/check-box user-parameter-name (in? list-owners user-id)))
+                      (hiccup-form/hidden-field user-parameter-name "on")]
+                     (hiccup-form/check-box user-parameter-name (in? list-owners user-id)))
                    [:label {:for user-parameter-name}
                     user-email-addr
                     (when (= (auth/current-user-id) user-id)
                       [:span.pill "you"])]]))
               (data/get-friendly-users-by-id (auth/current-user-id)))
@@ -156,34 +157,35 @@
      [:h1 "Component Lists"]
      [:div.component-lists
       (map (fn [ todo-list ]
              (let [ list-id (:todo_list_id todo-list) ]
                [:div
-                (form/check-box (str "list_" list-id)
-                                (in? view-sublist-ids list-id))
-                (:desc todo-list)]))
+                (hiccup-form/check-box (str "list_" list-id)
+                                       (in? view-sublist-ids list-id))
+                (hiccup-util/escape-html
+                 (:desc todo-list))]))
            (remove #(:is_view %) todo-lists))]]))
 
 (defn render-todo-list-details-page [ list-id min-list-priority & { :keys [ error-message ]}]
   (let [list-details (data/get-todo-list-by-id list-id)
         list-name (:desc list-details)
         is-view (:is_view list-details)
         list-type (if is-view "View" "List")]
     (render-page
-     {:title (str list-type " Details: " list-name)
+     {:title (str list-type " Details: " (hiccup-util/escape-html list-name))
       :sidebar (sidebar-view/render-sidebar-list-list list-id min-list-priority 0)}
      (scroll-column
       'todo-list-details-column
       [:h3
        [:a { :href (str "/list/" list-id ) } img-back-arrow]
-       "List Details: " list-name]
-      (form/form-to
+       "List Details: " (hiccup-util/escape-html list-name)]
+      (hiccup-form/form-to
        {:class "details"}
        [:post (shref "/list/" list-id "/details")]
        [:div.config-panel
         [:h1 (str list-type " Name:")]
-        (form/text-field { :maxlength "32" } "list-name" list-name)]
+        (hiccup-form/text-field { :maxlength "32" } "list-name" list-name)]
        (if is-view
          (render-todo-list-view-editor list-id)
          (render-todo-list-permissions list-id error-message))
        [:div.config-panel
         [:div
diff --git a/src/toto/view/page.clj b/src/toto/view/page.clj
index ff5040d..0ac9085 100644
--- a/src/toto/view/page.clj
+++ b/src/toto/view/page.clj
@@ -25,12 +25,13 @@
         toto.view.components
         toto.view.icons
         toto.view.query
         toto.view.components)
   (:require [clojure.tools.logging :as log]
-            [hiccup.page :as page]
-            [hiccup.form :as form]
+            [hiccup.page :as hiccup-page]
+            [hiccup.form :as hiccup-form]
+            [hiccup.util :as hiccup-util]
             [toto.view.auth :as auth]))
 
 (defn session-controls []
   (if-let [ username (auth/current-identity) ]
     [:div.session-controls
@@ -52,39 +53,39 @@
      [:dialog {:open "true" :data-escape-url escape-url}
       [:h3 (:title attrs)]
       [:div.cancel
        [:a {:href escape-url} img-window-close]]
       (if-let [ form-post-to (:form-post-to attrs)]
-        (form/form-to [:post form-post-to] contents)
+        (hiccup-form/form-to [:post form-post-to] contents)
         contents)]]))
 
 (defn- render-support-modal [ ]
   (let [ user-identity (auth/current-identity)]
     (render-modal
      {:title "Contact Support"
       :form-post-to "/support-message"}
      [:div.config-panel
       [:h1 "Contact Information"]
-      (form/text-field {:maxlength "128"
+      (hiccup-form/text-field {:maxlength "128"
                         :placeholder "Full Name"
                         :autocomplete "off"
                         :autofocus "on"}
                        "full-name")
-      (form/text-field (cond-> {:maxlength "128"
+      (hiccup-form/text-field (cond-> {:maxlength "128"
                                 :placeholder "E-Mail Address"
                                 :value user-identity}
                          user-identity (assoc :readonly "readonly"))
                        "email-address")]
      (render-verify-question)
      [:div.config-panel
       [:h1 "Message"]
-      (form/text-area {:maxlength "4096"
+      (hiccup-form/text-area {:maxlength "4096"
                        :rows "12"
                        :cols "64"
                        :autocomplete "off"}
                       "message-text")]
-     (form/hidden-field "current-uri" (shref))
+     (hiccup-form/hidden-field "current-uri" (shref))
      [:input {:type "submit" :value "Send Message"}])))
 
 (defn contact-support-button [ ]
   [:a {:href (shref "" {:modal "contact-support" })} "Contact Support"])
 
@@ -93,23 +94,23 @@
    [:meta {:name "viewport"
            ;; user-scalable=no fails to work on iOS n where n > 10
            :content "width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0"}]
    [:title (when *dev-mode* "DEV - ") (:name (:app *config*)) (when title (str " - " title))]
    [:link { :rel "shortcut icon" :href (resource "favicon.ico")}]
-   (page/include-css (resource "toto.css")
+   (hiccup-page/include-css (resource "toto.css")
                      (resource "font-awesome.min.css"))
    [:script {:type "module" :src (resource "toto.js")}]
-   (page/include-js (resource "DragDropTouch.js"))])
+   (hiccup-page/include-js (resource "DragDropTouch.js"))])
 
 (defn- render-header [ page-title show-menu? ]
   (let [ username (auth/current-identity)]
     [:div.header
      (when show-menu?
        [:span.toggle-menu img-show-list "&nbsp;"])
      [:span.app-name
       [:a { :href "/" } (:name (:app *config*))] " - "]
-     page-title
+     (hiccup-util/escape-html page-title)
      (when *dev-mode*
        [:span.pill.dev "DEV"])
      (session-controls)]))
 
 (defn- render-sidebar-footer []
@@ -147,9 +148,9 @@
      [:div.contents {:class (class-set { "with-sidebar" sidebar })}
       (render-page-modal attrs)
       contents]]))
 
 (defn render-page [ attrs & contents]
-  (page/html5
+  (hiccup-page/html5
    [:html
     (render-standard-header (:title attrs))
     (render-page-body attrs contents)]))

Vulnerable code:             [hiccup.form :as form]
            [hiccup.util :as util]
(defn ensure-string-breakpoints [ s n ]
  (clojure.string/join html-breakpoint (partition-string s n)))
    (-> (util/escape-html
    (form/form-to
       (form/hidden-field "item-list-id" list-id)
        (form/select-options (map (fn [ sublist ]
                                    [ (:desc sublist) (:sublist_id sublist)])
     (form/text-field (cond-> {:maxlength "1024"
     (form/hidden-field "item-priority" "0")
              created-by-name])]))]
   (form/select-options [[ "-" "-"]
   (form/form-to { :class "embedded "} [:get (shref "/list/" list-id)]
   (form/form-to { :class "embedded "} [:get (shref "/list/" list-id "/completions")]
         (:desc sublist-details)]]
   (form/select-options (map (fn [ list-info ]
                               [ (:desc list-info) (:todo_list_id list-info)])
                             (remove
                              #(= excluded-list-id (:todo_list_id %))
                              (data/get-todo-lists-by-user (auth/current-user-id)))))])
            [hiccup.form :as form]
  (form/form-to
   (form/text-field {:maxlength "32"
    (form/check-box "is-view" false "Y")
   (form/form-to {} [:post (shref "/list/" list-id "/sort")]
     (form/select-options [["Description" "desc"]
       (form/check-box "is-public" (:is_public list-details))
                      (form/hidden-field user-parameter-name "on")]
                     (form/check-box user-parameter-name (in? list-owners user-id)))
                (form/check-box (str "list_" list-id)
                                (in? view-sublist-ids list-id))
                (:desc todo-list)]))
     {:title (str list-type " Details: " list-name)
       "List Details: " list-name]
      (form/form-to
        (form/text-field { :maxlength "32" } "list-name" list-name)]
            [hiccup.page :as page]
            [hiccup.form :as form]
        (form/form-to [:post form-post-to] contents)
      (form/text-field {:maxlength "128"
      (form/text-field (cond-> {:maxlength "128"
      (form/text-area {:maxlength "4096"
     (form/hidden-field "current-uri" (shref))
   (page/include-css (resource "toto.css")
   (page/include-js (resource "DragDropTouch.js"))])
     page-title
  (page/html5
Fixed code:             [hiccup.form :as hiccup-form]
            [hiccup.util :as hiccup-util]
(defn- ensure-string-breakpoints [ s n ]
  (clojure.string/join html-breakpoint (map hiccup-util/escape-html (partition-string s n))))
    (-> (hiccup-util/escape-html
    (hiccup-form/form-to
       (hiccup-form/hidden-field "item-list-id" list-id)
        (hiccup-form/select-options (map (fn [ sublist ]
                                           [(hiccup-util/escape-html (:desc sublist))
                                            (:sublist_id sublist)])
     (hiccup-form/text-field (cond-> {:maxlength "1024"
     (hiccup-form/hidden-field "item-priority" "0")
              (hiccup-util/escape-html
               created-by-name)])]))]
   (hiccup-form/select-options [[ "-" "-"]
   (hiccup-form/form-to { :class "embedded "} [:get (shref "/list/" list-id)]
   (hiccup-form/form-to { :class "embedded "} [:get (shref "/list/" list-id "/completions")]
         (hiccup-util/escape-html
          (:desc sublist-details))]]
   (hiccup-form/select-options
    (map (fn [ list-info ]
           [(hiccup-util/escape-html (:desc list-info))
            (:todo_list_id list-info)])
         (remove
          #(= excluded-list-id (:todo_list_id %))
          (data/get-todo-lists-by-user (auth/current-user-id)))))])
            [hiccup.form :as hiccup-form]
            [hiccup.util :as hiccup-util]
  (hiccup-form/form-to
   (hiccup-form/text-field {:maxlength "32"
    (hiccup-form/check-box "is-view" false "Y")
   (hiccup-form/form-to {} [:post (shref "/list/" list-id "/sort")]
     (hiccup-form/select-options [["Description" "desc"]
       (hiccup-form/check-box "is-public" (:is_public list-details))
                      (hiccup-form/hidden-field user-parameter-name "on")]
                     (hiccup-form/check-box user-parameter-name (in? list-owners user-id)))
                (hiccup-form/check-box (str "list_" list-id)
                                       (in? view-sublist-ids list-id))
                (hiccup-util/escape-html
                 (:desc todo-list))]))
     {:title (str list-type " Details: " (hiccup-util/escape-html list-name))
       "List Details: " (hiccup-util/escape-html list-name)]
      (hiccup-form/form-to
        (hiccup-form/text-field { :maxlength "32" } "list-name" list-name)]
            [hiccup.page :as hiccup-page]
            [hiccup.form :as hiccup-form]
            [hiccup.util :as hiccup-util]
        (hiccup-form/form-to [:post form-post-to] contents)
      (hiccup-form/text-field {:maxlength "128"
      (hiccup-form/text-field (cond-> {:maxlength "128"
      (hiccup-form/text-area {:maxlength "4096"
     (hiccup-form/hidden-field "current-uri" (shref))
   (hiccup-page/include-css (resource "toto.css")
   (hiccup-page/include-js (resource "DragDropTouch.js"))])
     (hiccup-util/escape-html page-title)
  (hiccup-page/html5