Skip to content

Commit

Permalink
Merge pull request #57 from 418sec/1-other-simple-http-server
Browse files Browse the repository at this point in the history
Security Fix for Cross Site Request Forgery (CSRF) - huntr.dev
  • Loading branch information
TheWaWaR committed Apr 15, 2021
2 parents f932a3c + 8dccdf3 commit 892fb89
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@

# These are backup files generated by rustfmt
**/*.rs.bk

# IDE folders
.idea/
64 changes: 64 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
119 changes: 90 additions & 29 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(|_| ()))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(10)
.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: {},
Expand All @@ -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() {
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -485,26 +508,62 @@ 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))
{
return Err((
status::InternalServerError,
format!("Copy file failed: {}", errno),
));
} else {
println!(
" >> File saved: {}",
headers.filename.clone().unwrap()
);
}
// Pull out csrf field to check if token matches one generated
let csrf_field = match entries
.fields
.get("csrf")
.map(|fields| fields.first())
.unwrap_or(None)
{
Some(field) => field,
None => {
return Err((
status::BadRequest,
String::from("csrf parameter 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(())
Expand Down Expand Up @@ -738,16 +797,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()
Expand Down

0 comments on commit 892fb89

Please sign in to comment.