Skip to content

Commit 6e3edb0

Browse files
author
Cameron Clark
committed
Add CSRF token generation and matching for file upload requests
1 parent f932a3c commit 6e3edb0

File tree

5 files changed

+161
-28
lines changed

5 files changed

+161
-28
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77

88
# These are backup files generated by rustfmt
99
**/*.rs.bk
10+
11+
# IDE folders
12+
.idea/

Cargo.lock

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ chrono = "0.4.9"
1818
flate2 = "1.0.11"
1919
filetime = "0.2.7"
2020
pretty-bytes = "0.2.2"
21+
rand = "0.8.3"
2122
url = "2.1.0"
2223
hyper-native-tls = {version = "0.3.0", optional=true}
2324
mime_guess = "2.0"

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ FLAGS:
1818
--norange Disable header::Range support (partial request)
1919
--nosort Disable directory entries sort (by: name, modified, size)
2020
-s, --silent Disable all outputs
21-
-u, --upload Enable upload files (multiple select)
21+
-u, --upload Enable upload files (multiple select) (CSRF token required)
2222
-V, --version Prints version information
2323
2424
OPTIONS:
@@ -80,6 +80,7 @@ simple-http-server -h
8080
- [Range, If-Range, If-Match] => [Content-Range, 206, 416]
8181
- [x] (default disabled) Automatic render index page [index.html, index.htm]
8282
- [x] (default disabled) Upload file
83+
- A CSRF token is generated when upload is enabled and must be sent as a parameter when uploading a file
8384
- [x] (default disabled) HTTP Basic Authentication (by username:password)
8485
- [x] Sort by: filename, filesize, modifled
8586
- [x] HTTPS support

src/main.rs

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use open;
2727
use path_dedot::ParseDot;
2828
use percent_encoding::percent_decode;
2929
use pretty_bytes::converter::convert;
30+
use rand::distributions::Alphanumeric;
31+
use rand::{thread_rng, Rng};
3032
use termcolor::{Color, ColorSpec};
3133

3234
use color::{build_spec, Printer};
@@ -69,7 +71,7 @@ fn main() {
6971
.arg(clap::Arg::with_name("upload")
7072
.short("u")
7173
.long("upload")
72-
.help("Enable upload files (multiple select)"))
74+
.help("Enable upload files. (multiple select) (CSRF token required)"))
7375
.arg(clap::Arg::with_name("redirect").long("redirect")
7476
.takes_value(true)
7577
.validator(|url_string| iron::Url::parse(url_string.as_str()).map(|_| ()))
@@ -209,7 +211,7 @@ fn main() {
209211
.map(|s| PathBuf::from(s).canonicalize().unwrap())
210212
.unwrap_or_else(|| env::current_dir().unwrap());
211213
let index = matches.is_present("index");
212-
let upload = matches.is_present("upload");
214+
let upload_arg = matches.is_present("upload");
213215
let redirect_to = matches
214216
.value_of("redirect")
215217
.map(iron::Url::parse)
@@ -261,10 +263,22 @@ fn main() {
261263

262264
let silent = matches.is_present("silent");
263265

266+
let upload: Option<Upload> = if upload_arg {
267+
let token: String = thread_rng()
268+
.sample_iter(&Alphanumeric)
269+
.take(8)
270+
.map(char::from)
271+
.collect();
272+
Some(Upload { csrf_token: token })
273+
} else {
274+
None
275+
};
276+
264277
if !silent {
265278
printer
266279
.println_out(
267-
r#" Index: {}, Upload: {}, Cache: {}, Cors: {}, Range: {}, Sort: {}, Threads: {}
280+
r#" Index: {}, Cache: {}, Cors: {}, Range: {}, Sort: {}, Threads: {}
281+
Upload: {}, CSRF Token: {}
268282
Auth: {}, Compression: {}
269283
https: {}, Cert: {}, Cert-Password: {}
270284
Root: {},
@@ -273,12 +287,18 @@ fn main() {
273287
======== [{}] ========"#,
274288
&vec![
275289
enable_string(index),
276-
enable_string(upload),
277290
enable_string(cache),
278291
enable_string(cors),
279292
enable_string(range),
280293
enable_string(sort),
281294
threads.to_string(),
295+
enable_string(upload_arg),
296+
(if upload.is_some() {
297+
upload.as_ref().unwrap().csrf_token.as_str()
298+
} else {
299+
""
300+
})
301+
.to_string(),
282302
auth.unwrap_or("disabled").to_string(),
283303
compression_string,
284304
(if cert.is_some() {
@@ -381,11 +401,14 @@ fn main() {
381401
std::process::exit(1);
382402
};
383403
}
404+
struct Upload {
405+
csrf_token: String,
406+
}
384407

385408
struct MainHandler {
386409
root: PathBuf,
387410
index: bool,
388-
upload: bool,
411+
upload: Option<Upload>,
389412
cache: bool,
390413
range: bool,
391414
redirect_to: Option<iron::Url>,
@@ -433,7 +456,7 @@ impl Handler for MainHandler {
433456
));
434457
}
435458

436-
if self.upload && req.method == method::Post {
459+
if self.upload.is_some() && req.method == method::Post {
437460
if let Err((s, msg)) = self.save_files(req, &fs_path) {
438461
return Ok(error_resp(s, &msg));
439462
} else {
@@ -485,26 +508,65 @@ impl MainHandler {
485508
// in a new temporary directory under the OS temporary directory.
486509
match multipart.save().size_limit(self.upload_size_limit).temp() {
487510
SaveResult::Full(entries) => {
488-
for (_, fields) in entries.fields {
489-
for field in fields {
490-
let mut data = field.data.readable().unwrap();
491-
let headers = &field.headers;
492-
let mut target_path = path.clone();
493-
494-
target_path.push(headers.filename.clone().unwrap());
495-
if let Err(errno) = std::fs::File::create(target_path)
496-
.and_then(|mut file| io::copy(&mut data, &mut file))
497-
{
511+
// Pull out csrf field to check if token matches one generated
512+
let csrf_field = match entries.fields.get("csrf") {
513+
Some(fields) => match fields.first() {
514+
Some(field) => field,
515+
None => {
498516
return Err((
499-
status::InternalServerError,
500-
format!("Copy file failed: {}", errno),
501-
));
502-
} else {
503-
println!(
504-
" >> File saved: {}",
505-
headers.filename.clone().unwrap()
506-
);
517+
status::BadRequest,
518+
String::from("csrf token not provided"),
519+
))
507520
}
521+
},
522+
None => {
523+
return Err((
524+
status::BadRequest,
525+
String::from("csrf token not provided"),
526+
))
527+
}
528+
};
529+
530+
// Read token value from field
531+
let mut token = String::new();
532+
csrf_field
533+
.data
534+
.readable()
535+
.unwrap()
536+
.read_to_string(&mut token)
537+
.unwrap();
538+
539+
// Check if they match
540+
if self.upload.as_ref().unwrap().csrf_token != token {
541+
return Err((
542+
status::BadRequest,
543+
String::from("csrf token does not match"),
544+
));
545+
}
546+
547+
// Grab all the fields named files
548+
let files_fields = match entries.fields.get("files") {
549+
Some(fields) => fields,
550+
None => {
551+
return Err((status::BadRequest, String::from("no files provided")))
552+
}
553+
};
554+
555+
for field in files_fields {
556+
let mut data = field.data.readable().unwrap();
557+
let headers = &field.headers;
558+
let mut target_path = path.clone();
559+
560+
target_path.push(headers.filename.clone().unwrap());
561+
if let Err(errno) = std::fs::File::create(target_path)
562+
.and_then(|mut file| io::copy(&mut data, &mut file))
563+
{
564+
return Err((
565+
status::InternalServerError,
566+
format!("Copy file failed: {}", errno),
567+
));
568+
} else {
569+
println!(" >> File saved: {}", headers.filename.clone().unwrap());
508570
}
509571
}
510572
Ok(())
@@ -738,16 +800,18 @@ impl MainHandler {
738800
));
739801
}
740802

741-
// Optinal upload form
742-
let upload_form = if self.upload {
803+
// Optional upload form
804+
let upload_form = if self.upload.is_some() {
743805
format!(
744806
r#"
745807
<form style="margin-top:1em; margin-bottom:1em;" action="/{path}" method="POST" enctype="multipart/form-data">
746808
<input type="file" name="files" accept="*" multiple />
809+
<input type="hidden" name="csrf" value="{csrf}"/>
747810
<input type="submit" value="Upload" />
748811
</form>
749812
"#,
750-
path = encode_link_path(path_prefix)
813+
path = encode_link_path(path_prefix),
814+
csrf = self.upload.as_ref().unwrap().csrf_token
751815
)
752816
} else {
753817
"".to_owned()

0 commit comments

Comments
 (0)