Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Security Fix for Cross Site Request Forgery (CSRF) - huntr.dev #57

Merged
merged 4 commits into from
Apr 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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