Skip to content
Permalink
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Morgawr committed Apr 13, 2019
1 parent 15af8d2 commit c09ed97
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 18 deletions.
16 changes: 16 additions & 0 deletions 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

2 changes: 1 addition & 1 deletion create-db.sh
Expand Up @@ -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
2 changes: 1 addition & 1 deletion project.clj
@@ -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"]
Expand Down
76 changes: 60 additions & 16 deletions src/muon/handler.clj
Expand Up @@ -10,20 +10,24 @@
[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]
Expand All @@ -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 :*
Expand All @@ -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]
Expand All @@ -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]
Expand Down Expand Up @@ -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"}
Expand Down

0 comments on commit c09ed97

Please sign in to comment.