CVE-2019-25089 (published 2022-12-27T12:15Z)

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.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-330
cwe_name: Use of Insufficiently Random Values
description: The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.
extended_description: When product generates predictable values in a context requiring unpredictability, it may be possible for an attacker to guess the next value that will be generated, and use this guess to impersonate another user or access sensitive information.
         

Fix information:
repo_url: https://github.com/Morgawr/Muon
commit_details: repo_url: https://github.com/Morgawr/Muon
author: Morg
author_date: 2019-04-13 15:35:37+01:00
msg: 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.
repository: repo_url: https://github.com/Morgawr/Muon
repo_name: Morgawr/Muon
description: Minimalistic private self-destructible file hosting webserver written in Clojure
repo_language: Clojure
owner: Morgawr
file_changes: filename: build-wordlist.sh
change_type: ModificationType.ADD
diff: @@ -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
+

programming_language: Shell
filename: create-db.sh
change_type: ModificationType.MODIFY
diff: @@ -3,6 +3,6 @@
 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

programming_language: Shell
filename: project.clj
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,4 @@
-(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"]

programming_language: Ruby
filename: handler.clj
change_type: ModificationType.MODIFY
diff: @@ -10,12 +10,16 @@
             [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")
@@ -23,7 +27,7 @@
 
 (defn build-url
   [& args]
-  (reduce str DOMAIN_ROOT args))
+  (reduce str DOMAIN_ROOT (interpose "/" args)))
 
 (defn pool
   [spec]
@@ -42,6 +46,11 @@
 (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 :*
@@ -58,22 +67,40 @@
    :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]
@@ -82,11 +109,23 @@
         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]
@@ -120,13 +159,18 @@
                    (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"}

programming_language: Haskell