From 6e3edb0ea1f5ec2dfc7ff723d09fce2ebeb64084 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Wed, 7 Apr 2021 23:59:56 -0400 Subject: [PATCH 1/3] 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 = 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, cache: bool, range: bool, redirect_to: Option, @@ -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#"
+
"#, - path = encode_link_path(path_prefix) + path = encode_link_path(path_prefix), + csrf = self.upload.as_ref().unwrap().csrf_token ) } else { "".to_owned() From c0e323947eedb383741b9495ee4a9c0f784553f8 Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Thu, 8 Apr 2021 00:08:30 -0400 Subject: [PATCH 2/3] Better error message, longer token --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 612b940..5ca5c6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -266,7 +266,7 @@ fn main() { let upload: Option = if upload_arg { let token: String = thread_rng() .sample_iter(&Alphanumeric) - .take(8) + .take(10) .map(char::from) .collect(); Some(Upload { csrf_token: token }) @@ -515,14 +515,14 @@ impl MainHandler { None => { return Err(( status::BadRequest, - String::from("csrf token not provided"), + String::from("csrf parameter not provided"), )) } }, None => { return Err(( status::BadRequest, - String::from("csrf token not provided"), + String::from("csrf parameter not provided"), )) } }; From 6b192690bd5512aa64f6b48734ff062257a4d78c Mon Sep 17 00:00:00 2001 From: Cameron Clark Date: Fri, 9 Apr 2021 12:14:46 -0400 Subject: [PATCH 3/3] Update csrf_field matching --- src/main.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5ca5c6c..67f67ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -509,16 +509,13 @@ impl MainHandler { match multipart.save().size_limit(self.upload_size_limit).temp() { SaveResult::Full(entries) => { // 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::BadRequest, - String::from("csrf parameter not provided"), - )) - } - }, + let csrf_field = match entries + .fields + .get("csrf") + .map(|fields| fields.first()) + .unwrap_or(None) + { + Some(field) => field, None => { return Err(( status::BadRequest,