CVE-2019-25089: Use of Insufficiently Random Values (CWE-330)
value: A vulnerability has been found in Morgawr Muon 0.1.1 and classified as problematic. Affected by this vulnerability is an unknown functionality of the file src/muon/handler.clj. The manipulation leads to insufficiently random values. The attack can be launched remotely. Upgrading to version 0.2.0-indev is able to address this issue. The name of the patch is c09ed972c020f759110c707b06ca2644f0bacd7f. It is recommended to upgrade the affected component. The identifier VDB-216877 was assigned to this vulnerability.
CWE description: The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.
CVSS v3 base score: 7.5
Published: 2022-12-27T12:15Z
Repository: https://github.com/Morgawr/Muon
Language: Other
Files changed: build-wordlist.sh, create-db.sh, project.clj, src/muon/handler.clj
Commit date: 2019-04-13 15:35:37 +0100
Commit message: Fix #4: Use randomized URLs

With this commit we fix the vulnerability attack described in #4.
Now files are stored in their own semi-unique folder locations generated
by a list of dictionary words, randomized at runtime.

Also this commit introduces 0.2.0-indev version with initial groundwork
for automated file expiration and collection.

Diff: diff --git a/build-wordlist.sh b/build-wordlist.sh
new file mode 100755
index 0000000..ad9af5f
--- /dev/null
+++ b/build-wordlist.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+function gen_dict() {
+	DICT=$1
+
+}
+DICTFILE=/usr/share/dict/words
+OUTFILE=db/wordlist
+if test -f "$DICTFILE"; then
+	# Get all words longer than 5 characters and save them in their own wordlist file
+	awk 'length>5' $DICTFILE | uniq > $OUTFILE
+else
+	(>&2 echo "Error: The file ${DICTFILE} does not exist. Cannot generate wordlist for file indexing.")
+	exit 1
+fi
+
diff --git a/create-db.sh b/create-db.sh
index 029ddf9..85f940a 100755
--- a/create-db.sh
+++ b/create-db.sh
@@ -1,8 +1,8 @@
 #!/usr/bin/env bash
 
 mkdir db 2> /dev/null
 rm db/database.db 2> /dev/null
 sqlite3 db/database.db << EOF
-create table data(id integer primary key autoincrement, folder text, filename text, text text, type text, policy text, expires_at timestamp, max_visits int, visits int);
+create table data(id integer primary key autoincrement, folder text, filename text, type text, policy text, expires_at timestamp, max_visits int, visits int);
 create table autoexpire(id integer primary key autoincrement, data_id integer, expires_at timestamp);
 EOF
diff --git a/project.clj b/project.clj
index a8d2251..708a1b6 100644
--- a/project.clj
+++ b/project.clj
@@ -1,6 +1,6 @@
-(defproject muon "0.1.1"
+(defproject muon "0.2.0-indev"
   :description "Muon - Build your own self-destructible file host."
   :url "https://github.com/Morgawr/Muon.git"
   :dependencies [[org.clojure/clojure "1.9.0"]
                  [compojure "1.6.1"]
                  [org.xerial/sqlite-jdbc "3.7.2"]
diff --git a/src/muon/handler.clj b/src/muon/handler.clj
index b9a437e..0ac5bb3 100644
--- a/src/muon/handler.clj
+++ b/src/muon/handler.clj
@@ -8,24 +8,28 @@
             [clojure.java.io :as io]
             [honeysql.core :as hn]
             [clojure.java.jdbc :as db]))
 
 ; Modify this if you want to change the url returned by the
-; webserver
+; webserver.
 (def DOMAIN_ROOT "http://localhost:8080/")
 
 ; Change the password!!
 (def PASSWORD "password")
 
+; Change this if you want to use a different file to source
+; the randomized wordlists for your system.
+(def WORDSFILE "db/wordlist")
+
 ; Default Content-Type
 (def MIME "application/octet-stream")
 (def MIMEFILE "/etc/mime.types")
 (def mimetypes nil)
 
 (defn build-url
   [& args]
-  (reduce str DOMAIN_ROOT args))
+  (reduce str DOMAIN_ROOT (interpose "/" args)))
 
 (defn pool
   [spec]
   (let [cpds (doto (ComboPooledDataSource.)
                (.setDriverClass (:classname spec))
@@ -40,10 +44,15 @@
             :subname "db/database.db"})
 
 (def pooled-db (delay (pool sqldb)))
 (defn db-connection [] @pooled-db)
 
+(defn get-words-file []
+  (clojure.string/split-lines (slurp WORDSFILE)))
+
+(def words-file (memoize get-words-file))
+
 (defn get-from-db
   [id]
   (first (db/query (db-connection) (hn/format (hn/build {:select :*
                                                          :from [:data]
                                                          :where [:= :id id]})))))
@@ -56,39 +65,69 @@
 (def internal-error
   {:status 500
    :headers {}
    :body "Internal server error, this should NOT happen."})
 
+(defn generate-random-folder []
+  (let [words (words-file)]
+    (str (rand-nth words) (rand-nth words) (rand-nth words))))
+
+(def base-file-data {:folder ""
+                     :filename ""
+                     :type ""
+                     :visits 0
+                     :expires_at 0
+                     :max_visits 0
+                     :policy ""})
+
 (defn save-to-db
-  [text type opts]
+  [folder filename type opts]
   (let [duration (try (Integer/parseInt (:duration opts)) (catch Exception e nil))
-        clicks (try (Integer/parseInt (:clicks opts)) (catch Exception e nil))]
+        clicks (try (Integer/parseInt (:clicks opts)) (catch Exception e nil))
+        data (merge base-file-data {:folder folder :filename filename :type type})
+        get-id #(last (ffirst %))
+        save-fn (fn [query expires?] 
+                  (let [res (db/insert! (db-connection) :data query)]
+                    (when expires?
+                      ; Set up auto-expire entry in the table
+                      (db/insert! (db-connection) :autoexpire {:data_id (get-id res) 
+                                                               :expires_at (:expires_at query)}))
+                    res))
+        get-url #(build-url "resource" folder filename)]
     (cond
      (and (nil? duration) (nil? clicks)) wrong-options
      (not (nil? clicks))
-       (let [res (db/insert! (db-connection) :data {:text text :type type :policy "clicks" :expires_at 0 :max_visits clicks :visits 0})]
-         (build-url
-          "resource/"
-          (str (last (ffirst res)) "\n")))
+       (let [res (save-fn (merge data {:policy "clicks" :max_visits clicks}) false)]
+         (str (get-url) "\n"))
      (not (nil? duration))
-       (let [res (db/insert! (db-connection) :data {:text text :type type :policy "timed" :expires_at (+ (System/currentTimeMillis) (* duration 1000)) :max_visits 0 :visits 0})]
-         (build-url
-          "resource/"
-          (str (last (ffirst res)) "\n")))
+       (let [res (save-fn (merge data {:policy "timed" :expires_at (+ (System/currentTimeMillis) (* duration 1000))}) true)]
+         (str (get-url) "\n"))
      :else internal-error)))
 
 (defn get-mime [filename]
   (let [dot (.indexOf filename ".")
         ext (if (> dot -1) (subs filename (inc dot)))
         mime (or (get mimetypes ext) MIME)]
     mime))
 
+; TODO(morg): Make exit condition with exception if we recur too many times.
+(defn randomize-file-location [filename]
+  (loop []
+    (let [folder (generate-random-folder)
+          full-name (reduce str (interpose "/" ["resources" folder filename]))
+          new-file (io/as-file full-name)]
+      (do
+        (io/make-parents full-name)
+        (if (not (.exists new-file))
+          [folder new-file]
+          (recur))))))
+
 (defn handle-file-upload
   [data]
-  (let [filename (str "resources/" (System/currentTimeMillis))]
-    (io/copy (:tempfile (:file data)) (io/as-file filename))
-    (save-to-db filename (get-mime (:filename (:file data))) data)))
+  (let [[folder file] (randomize-file-location (:filename (:file data)))]
+    (io/copy (:tempfile (:file data)) file)
+    (save-to-db folder (:filename (:file data)) (get-mime (:filename (:file data))) data)))
 
 (defn build-response
   [text type]
   (if (seq type)
     (res/content-type (res/file-response text) type)
@@ -118,17 +157,22 @@
   [file]
   (into {} (map #(let [line (.split % "\\s+")]
                    (zipmap (rest line) (repeat (first line))))
                 (clojure.string/split (slurp file) #"\n"))))
 
+
+(defn check-password [password]
+  (not= password PASSWORD))
+
 (defroutes app-routes
   (GET "/" [] "Welcome to Muon, the private self-destructible file host.")
+  ; TODO(morg) Remove the /resource/ path and only access $HOST/<folder>/<filename> 
   (GET ["/resource/:id" :id #"[0-9]+"] [id] (return-data (Integer/parseInt id)))
   (mp/wrap-multipart-params
    (POST "/upload" {params :params}
          (cond
-           (not= (:password params) PASSWORD)
+           (check-password (:password params))
              {:status 401
               :header {}
               :body "Unauthorized access. This incident will be reported to your parents.\n"}
            (not (nil? (:file params)))
              (handle-file-upload params)

Vulnerable code: create table data(id integer primary key autoincrement, folder text, filename text, text text, type text, policy text, expires_at timestamp, max_visits int, visits int);
(defproject muon "0.1.1"
; webserver
  (reduce str DOMAIN_ROOT args))
  [text type opts]
        clicks (try (Integer/parseInt (:clicks opts)) (catch Exception e nil))]
       (let [res (db/insert! (db-connection) :data {:text text :type type :policy "clicks" :expires_at 0 :max_visits clicks :visits 0})]
         (build-url
          "resource/"
          (str (last (ffirst res)) "\n")))
       (let [res (db/insert! (db-connection) :data {:text text :type type :policy "timed" :expires_at (+ (System/currentTimeMillis) (* duration 1000)) :max_visits 0 :visits 0})]
         (build-url
          "resource/"
          (str (last (ffirst res)) "\n")))
  (let [filename (str "resources/" (System/currentTimeMillis))]
    (io/copy (:tempfile (:file data)) (io/as-file filename))
    (save-to-db filename (get-mime (:filename (:file data))) data)))
           (not= (:password params) PASSWORD)
Fixed code: #!/usr/bin/env bash

function gen_dict() {
	DICT=$1

}
DICTFILE=/usr/share/dict/words
OUTFILE=db/wordlist
if test -f "$DICTFILE"; then
	# Get all words longer than 5 characters and save them in their own wordlist file
	awk 'length>5' $DICTFILE | uniq > $OUTFILE
else
	(>&2 echo "Error: The file ${DICTFILE} does not exist. Cannot generate wordlist for file indexing.")
	exit 1
fi

create table data(id integer primary key autoincrement, folder text, filename text, type text, policy text, expires_at timestamp, max_visits int, visits int);
(defproject muon "0.2.0-indev"
; webserver.
; Change this if you want to use a different file to source
; the randomized wordlists for your system.
(def WORDSFILE "db/wordlist")

  (reduce str DOMAIN_ROOT (interpose "/" args)))
(defn get-words-file []
  (clojure.string/split-lines (slurp WORDSFILE)))

(def words-file (memoize get-words-file))

(defn generate-random-folder []
  (let [words (words-file)]
    (str (rand-nth words) (rand-nth words) (rand-nth words))))

(def base-file-data {:folder ""
                     :filename ""
                     :type ""
                     :visits 0
                     :expires_at 0
                     :max_visits 0
                     :policy ""})

  [folder filename type opts]
        clicks (try (Integer/parseInt (:clicks opts)) (catch Exception e nil))
        data (merge base-file-data {:folder folder :filename filename :type type})
        get-id #(last (ffirst %))
        save-fn (fn [query expires?] 
                  (let [res (db/insert! (db-connection) :data query)]
                    (when expires?
                      ; Set up auto-expire entry in the table
                      (db/insert! (db-connection) :autoexpire {:data_id (get-id res) 
                                                               :expires_at (:expires_at query)}))
                    res))
        get-url #(build-url "resource" folder filename)]
       (let [res (save-fn (merge data {:policy "clicks" :max_visits clicks}) false)]
         (str (get-url) "\n"))
       (let [res (save-fn (merge data {:policy "timed" :expires_at (+ (System/currentTimeMillis) (* duration 1000))}) true)]
         (str (get-url) "\n"))
; TODO(morg): Make exit condition with exception if we recur too many times.
(defn randomize-file-location [filename]
  (loop []
    (let [folder (generate-random-folder)
          full-name (reduce str (interpose "/" ["resources" folder filename]))
          new-file (io/as-file full-name)]
      (do
        (io/make-parents full-name)
        (if (not (.exists new-file))
          [folder new-file]
          (recur))))))

  (let [[folder file] (randomize-file-location (:filename (:file data)))]
    (io/copy (:tempfile (:file data)) file)
    (save-to-db folder (:filename (:file data)) (get-mime (:filename (:file data))) data)))

(defn check-password [password]
  (not= password PASSWORD))

  ; TODO(morg) Remove the /resource/ path and only access $HOST/<folder>/<filename> 
           (check-password (:password params))