Skip to content

Commit 892fb89

Browse files
authored
Merge pull request #57 from 418sec/1-other-simple-http-server
Security Fix for Cross Site Request Forgery (CSRF) - huntr.dev
2 parents f932a3c + 8dccdf3 commit 892fb89

File tree

5 files changed

+160
-30
lines changed

5 files changed

+160
-30
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: 90 additions & 29 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(10)
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,62 @@ 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-
{
498-
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-
);
507-
}
511+
// Pull out csrf field to check if token matches one generated
512+
let csrf_field = match entries
513+
.fields
514+
.get("csrf")
515+
.map(|fields| fields.first())
516+
.unwrap_or(None)
517+
{
518+
Some(field) => field,
519+
None => {
520+
return Err((
521+
status::BadRequest,
522+
String::from("csrf parameter not provided"),
523+
))
524+
}
525+
};
526+
527+
// Read token value from field
528+
let mut token = String::new();
529+
csrf_field
530+
.data
531+
.readable()
532+
.unwrap()
533+
.read_to_string(&mut token)
534+
.unwrap();
535+
536+
// Check if they match
537+
if self.upload.as_ref().unwrap().csrf_token != token {
538+
return Err((
539+
status::BadRequest,
540+
String::from("csrf token does not match"),
541+
));
542+
}
543+
544+
// Grab all the fields named files
545+
let files_fields = match entries.fields.get("files") {
546+
Some(fields) => fields,
547+
None => {
548+
return Err((status::BadRequest, String::from("no files provided")))
549+
}
550+
};
551+
552+
for field in files_fields {
553+
let mut data = field.data.readable().unwrap();
554+
let headers = &field.headers;
555+
let mut target_path = path.clone();
556+
557+
target_path.push(headers.filename.clone().unwrap());
558+
if let Err(errno) = std::fs::File::create(target_path)
559+
.and_then(|mut file| io::copy(&mut data, &mut file))
560+
{
561+
return Err((
562+
status::InternalServerError,
563+
format!("Copy file failed: {}", errno),
564+
));
565+
} else {
566+
println!(" >> File saved: {}", headers.filename.clone().unwrap());
508567
}
509568
}
510569
Ok(())
@@ -738,16 +797,18 @@ impl MainHandler {
738797
));
739798
}
740799

741-
// Optinal upload form
742-
let upload_form = if self.upload {
800+
// Optional upload form
801+
let upload_form = if self.upload.is_some() {
743802
format!(
744803
r#"
745804
<form style="margin-top:1em; margin-bottom:1em;" action="/{path}" method="POST" enctype="multipart/form-data">
746805
<input type="file" name="files" accept="*" multiple />
806+
<input type="hidden" name="csrf" value="{csrf}"/>
747807
<input type="submit" value="Upload" />
748808
</form>
749809
"#,
750-
path = encode_link_path(path_prefix)
810+
path = encode_link_path(path_prefix),
811+
csrf = self.upload.as_ref().unwrap().csrf_token
751812
)
752813
} else {
753814
"".to_owned()

0 commit comments

Comments
 (0)