From 6e3edb0ea1f5ec2dfc7ff723d09fce2ebeb64084 Mon Sep 17 00:00:00 2001
From: Cameron Clark <cameron@gremlin.com>
Date: Wed, 7 Apr 2021 23:59:56 -0400
Subject: [PATCH] Add CSRF token generation and matching for file upload
 requests

---
 .gitignore  |   3 ++
 Cargo.lock  |  64 ++++++++++++++++++++++++++++
 Cargo.toml  |   1 +
 README.md   |   3 +-
 src/main.rs | 118 ++++++++++++++++++++++++++++++++++++++++------------
 5 files changed, 161 insertions(+), 28 deletions(-)

diff --git a/.gitignore b/.gitignore
index 3bbbd83..2abe554 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,6 @@
 
 # These are backup files generated by rustfmt
 **/*.rs.bk
+
+# IDE folders
+.idea/
diff --git a/Cargo.lock b/Cargo.lock
index ae216c0..05dafe8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -89,6 +89,11 @@ name = "cfg-if"
 version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "chrono"
 version = "0.4.13"
@@ -206,6 +211,16 @@ dependencies = [
  "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "getrandom"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)",
+ "wasi 0.10.2+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "groupable"
 version = "0.2.0"
@@ -639,6 +654,17 @@ dependencies = [
  "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.72 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_chacha 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_hc 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_chacha"
 version = "0.1.1"
@@ -657,6 +683,15 @@ dependencies = [
  "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.3.1"
@@ -678,6 +713,14 @@ dependencies = [
  "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand_core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_hc"
 version = "0.1.0"
@@ -694,6 +737,14 @@ dependencies = [
  "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rand_hc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rand_isaac"
 version = "0.1.1"
@@ -853,6 +904,7 @@ dependencies = [
  "path-dedot 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
  "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "pretty-bytes 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1047,6 +1099,11 @@ name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -1089,6 +1146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
 "checksum cc 1.0.58 (registry+https://github.com/rust-lang/crates.io-index)" = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518"
 "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+"checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 "checksum chrono 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6"
 "checksum chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "498d20a7aaf62625b9bf26e637cf7736417cde1d0c99f1d04d1170229a85cf87"
 "checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
@@ -1103,6 +1161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 "checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
 "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
+"checksum getrandom 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
 "checksum groupable 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "32619942b8be646939eaf3db0602b39f5229b74575b67efc897811ded1db4e57"
 "checksum hermit-abi 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9"
 "checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
@@ -1152,13 +1211,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
 "checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+"checksum rand 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
 "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
 "checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+"checksum rand_chacha 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
 "checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
 "checksum rand_core 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
 "checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+"checksum rand_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
 "checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
 "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+"checksum rand_hc 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
 "checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
 "checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
 "checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
@@ -1201,6 +1264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
 "checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+"checksum wasi 0.10.2+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
 "checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
 "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
diff --git a/Cargo.toml b/Cargo.toml
index f7967c7..4790cc3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,7 @@ chrono = "0.4.9"
 flate2 = "1.0.11"
 filetime = "0.2.7"
 pretty-bytes = "0.2.2"
+rand = "0.8.3"
 url = "2.1.0"
 hyper-native-tls = {version = "0.3.0", optional=true}
 mime_guess = "2.0"
diff --git a/README.md b/README.md
index fd374dc..529b3f0 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ FLAGS:
         --norange    Disable header::Range support (partial request)
         --nosort     Disable directory entries sort (by: name, modified, size)
     -s, --silent     Disable all outputs
-    -u, --upload     Enable upload files (multiple select)
+    -u, --upload     Enable upload files (multiple select) (CSRF token required)
     -V, --version    Prints version information
 
 OPTIONS:
@@ -80,6 +80,7 @@ simple-http-server -h
   - [Range, If-Range, If-Match] => [Content-Range, 206, 416]
 - [x] (default disabled) Automatic render index page [index.html, index.htm]
 - [x] (default disabled) Upload file
+  - A CSRF token is generated when upload is enabled and must be sent as a parameter when uploading a file
 - [x] (default disabled) HTTP Basic Authentication (by username:password)
 - [x] Sort by: filename, filesize, modifled
 - [x] HTTPS support
diff --git a/src/main.rs b/src/main.rs
index 2e1a35d..612b940 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -27,6 +27,8 @@ use open;
 use path_dedot::ParseDot;
 use percent_encoding::percent_decode;
 use pretty_bytes::converter::convert;
+use rand::distributions::Alphanumeric;
+use rand::{thread_rng, Rng};
 use termcolor::{Color, ColorSpec};
 
 use color::{build_spec, Printer};
@@ -69,7 +71,7 @@ fn main() {
         .arg(clap::Arg::with_name("upload")
              .short("u")
              .long("upload")
-             .help("Enable upload files (multiple select)"))
+             .help("Enable upload files. (multiple select) (CSRF token required)"))
         .arg(clap::Arg::with_name("redirect").long("redirect")
              .takes_value(true)
              .validator(|url_string| iron::Url::parse(url_string.as_str()).map(|_| ()))
@@ -209,7 +211,7 @@ fn main() {
         .map(|s| PathBuf::from(s).canonicalize().unwrap())
         .unwrap_or_else(|| env::current_dir().unwrap());
     let index = matches.is_present("index");
-    let upload = matches.is_present("upload");
+    let upload_arg = matches.is_present("upload");
     let redirect_to = matches
         .value_of("redirect")
         .map(iron::Url::parse)
@@ -261,10 +263,22 @@ fn main() {
 
     let silent = matches.is_present("silent");
 
+    let upload: Option<Upload> = if upload_arg {
+        let token: String = thread_rng()
+            .sample_iter(&Alphanumeric)
+            .take(8)
+            .map(char::from)
+            .collect();
+        Some(Upload { csrf_token: token })
+    } else {
+        None
+    };
+
     if !silent {
         printer
             .println_out(
-                r#"     Index: {}, Upload: {}, Cache: {}, Cors: {}, Range: {}, Sort: {}, Threads: {}
+                r#"     Index: {}, Cache: {}, Cors: {}, Range: {}, Sort: {}, Threads: {}
+          Upload: {}, CSRF Token: {}
           Auth: {}, Compression: {}
          https: {}, Cert: {}, Cert-Password: {}
           Root: {},
@@ -273,12 +287,18 @@ fn main() {
     ======== [{}] ========"#,
                 &vec![
                     enable_string(index),
-                    enable_string(upload),
                     enable_string(cache),
                     enable_string(cors),
                     enable_string(range),
                     enable_string(sort),
                     threads.to_string(),
+                    enable_string(upload_arg),
+                    (if upload.is_some() {
+                        upload.as_ref().unwrap().csrf_token.as_str()
+                    } else {
+                        ""
+                    })
+                    .to_string(),
                     auth.unwrap_or("disabled").to_string(),
                     compression_string,
                     (if cert.is_some() {
@@ -381,11 +401,14 @@ fn main() {
         std::process::exit(1);
     };
 }
+struct Upload {
+    csrf_token: String,
+}
 
 struct MainHandler {
     root: PathBuf,
     index: bool,
-    upload: bool,
+    upload: Option<Upload>,
     cache: bool,
     range: bool,
     redirect_to: Option<iron::Url>,
@@ -433,7 +456,7 @@ impl Handler for MainHandler {
             ));
         }
 
-        if self.upload && req.method == method::Post {
+        if self.upload.is_some() && req.method == method::Post {
             if let Err((s, msg)) = self.save_files(req, &fs_path) {
                 return Ok(error_resp(s, &msg));
             } else {
@@ -485,26 +508,65 @@ impl MainHandler {
                 // in a new temporary directory under the OS temporary directory.
                 match multipart.save().size_limit(self.upload_size_limit).temp() {
                     SaveResult::Full(entries) => {
-                        for (_, fields) in entries.fields {
-                            for field in fields {
-                                let mut data = field.data.readable().unwrap();
-                                let headers = &field.headers;
-                                let mut target_path = path.clone();
-
-                                target_path.push(headers.filename.clone().unwrap());
-                                if let Err(errno) = std::fs::File::create(target_path)
-                                    .and_then(|mut file| io::copy(&mut data, &mut file))
-                                {
+                        // Pull out csrf field to check if token matches one generated
+                        let csrf_field = match entries.fields.get("csrf") {
+                            Some(fields) => match fields.first() {
+                                Some(field) => field,
+                                None => {
                                     return Err((
-                                        status::InternalServerError,
-                                        format!("Copy file failed: {}", errno),
-                                    ));
-                                } else {
-                                    println!(
-                                        "  >> File saved: {}",
-                                        headers.filename.clone().unwrap()
-                                    );
+                                        status::BadRequest,
+                                        String::from("csrf token not provided"),
+                                    ))
                                 }
+                            },
+                            None => {
+                                return Err((
+                                    status::BadRequest,
+                                    String::from("csrf token not provided"),
+                                ))
+                            }
+                        };
+
+                        // Read token value from field
+                        let mut token = String::new();
+                        csrf_field
+                            .data
+                            .readable()
+                            .unwrap()
+                            .read_to_string(&mut token)
+                            .unwrap();
+
+                        // Check if they match
+                        if self.upload.as_ref().unwrap().csrf_token != token {
+                            return Err((
+                                status::BadRequest,
+                                String::from("csrf token does not match"),
+                            ));
+                        }
+
+                        // Grab all the fields named files
+                        let files_fields = match entries.fields.get("files") {
+                            Some(fields) => fields,
+                            None => {
+                                return Err((status::BadRequest, String::from("no files provided")))
+                            }
+                        };
+
+                        for field in files_fields {
+                            let mut data = field.data.readable().unwrap();
+                            let headers = &field.headers;
+                            let mut target_path = path.clone();
+
+                            target_path.push(headers.filename.clone().unwrap());
+                            if let Err(errno) = std::fs::File::create(target_path)
+                                .and_then(|mut file| io::copy(&mut data, &mut file))
+                            {
+                                return Err((
+                                    status::InternalServerError,
+                                    format!("Copy file failed: {}", errno),
+                                ));
+                            } else {
+                                println!("  >> File saved: {}", headers.filename.clone().unwrap());
                             }
                         }
                         Ok(())
@@ -738,16 +800,18 @@ impl MainHandler {
             ));
         }
 
-        // Optinal upload form
-        let upload_form = if self.upload {
+        // Optional upload form
+        let upload_form = if self.upload.is_some() {
             format!(
                 r#"
 <form style="margin-top:1em; margin-bottom:1em;" action="/{path}" method="POST" enctype="multipart/form-data">
   <input type="file" name="files" accept="*" multiple />
+  <input type="hidden" name="csrf" value="{csrf}"/>
   <input type="submit" value="Upload" />
 </form>
 "#,
-                path = encode_link_path(path_prefix)
+                path = encode_link_path(path_prefix),
+                csrf = self.upload.as_ref().unwrap().csrf_token
             )
         } else {
             "".to_owned()
-- 
GitLab