Skip to content

Commit

Permalink
fix: against traversal attack
Browse files Browse the repository at this point in the history
  • Loading branch information
LeSim committed Jul 3, 2023
1 parent 20d49ed commit 586c60c
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 20 deletions.
94 changes: 77 additions & 17 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,23 @@ fn normalize_and_parse_upstream_url(mut url: String) -> Url {
}

impl HttpConfig {
pub fn create_upstream_url(&self, req: &HttpRequest) -> String {
let base = Url::parse(self.upstream_base_url.as_ref()).unwrap();
let mut url = base.join(&req.match_info()["name"]).unwrap();
pub fn create_upstream_url(&self, req: &HttpRequest) -> Option<String> {
// Warning: join process '../'
// "https://a.com/jail/".join('../escape') => "https://a.com/escape"
let mut url = self
.upstream_base_url
.join(&req.match_info()["name"])
.unwrap();

if self.is_traversal_attack(&url) {
return None;
}

if !req.query_string().is_empty() {
url.set_query(Some(req.query_string()));
}

url.to_string()
Some(url.to_string())
}

pub fn local_encryption_path_for(&self, req: &HttpRequest) -> PathBuf {
Expand All @@ -187,6 +195,27 @@ impl HttpConfig {
filepath.push(name);
filepath
}

fn is_traversal_attack(&self, url: &Url) -> bool {
// https://upstream.com => [Some("")]
// https://upstream.com/jail/cell/ => [Some("jail"), Some("cell"), Some("")]
let mut base_segments: Vec<&str> =
self.upstream_base_url.path_segments().unwrap().collect();

// remove the last segment corresponding to "/"
base_segments.pop();

let mut url_segments = url.path_segments().unwrap();

// ensure that all segment of the upstream_base_url
// are present in the final url
let safe = base_segments.iter().all(|base_segment| {
let url_segment = url_segments.next().unwrap();
base_segment == &url_segment
});

!safe
}
}

fn read_file_content(path_string: &str) -> String {
Expand All @@ -213,34 +242,65 @@ mod tests {

#[test]
fn test_create_upstream_url() {
let req = TestRequest::default()
.uri("https://proxy.com/bucket/file.zip?p1=ok1&p2=ok2")
.param("name", "bucket/file.zip") // hack to force parsing
let base = "https://upstream.com/";
let jailed_base = "https://upstream.com/jail/cell/";

let config = default_config(base);
let jailed_config = default_config(jailed_base);

let file = TestRequest::default()
.uri("https://proxy.com/file")
.param("name", "file") // hack to force parsing
.to_http_request();

assert_eq!(
default_config("https://upstream.com").create_upstream_url(&req),
"https://upstream.com/bucket/file.zip?p1=ok1&p2=ok2"
config.create_upstream_url(&file),
Some("https://upstream.com/file".to_string())
);

assert_eq!(
default_config("https://upstream.com/").create_upstream_url(&req),
"https://upstream.com/bucket/file.zip?p1=ok1&p2=ok2"
jailed_config.create_upstream_url(&file),
Some("https://upstream.com/jail/cell/file".to_string())
);

let sub_dir_file = TestRequest::default()
.uri("https://proxy.com/sub/dir/file")
.param("name", "sub/dir/file") // hack to force parsing
.to_http_request();

assert_eq!(
config.create_upstream_url(&sub_dir_file),
Some("https://upstream.com/sub/dir/file".to_string())
);

assert_eq!(
default_config("https://upstream.com/sub_folder/").create_upstream_url(&req),
"https://upstream.com/sub_folder/bucket/file.zip?p1=ok1&p2=ok2"
jailed_config.create_upstream_url(&sub_dir_file),
Some("https://upstream.com/jail/cell/sub/dir/file".to_string())
);

let req = TestRequest::default()
.uri("https://proxy.com/bucket/file.zip")
let path_traversal_file = TestRequest::default()
.uri("https://proxy.com/../escape")
.param("name", "../escape") // hack to force parsing
.to_http_request();

assert_eq!(
config.create_upstream_url(&path_traversal_file),
Some("https://upstream.com/escape".to_string())
);

assert_eq!(
jailed_config.create_upstream_url(&path_traversal_file),
None
);

let file_with_query_string = TestRequest::default()
.uri("https://proxy.com/bucket/file.zip?p1=ok1&p2=ok2")
.param("name", "bucket/file.zip") // hack to force parsing
.to_http_request();

assert_eq!(
default_config("https://upstream.com").create_upstream_url(&req),
"https://upstream.com/bucket/file.zip"
config.create_upstream_url(&file_with_query_string),
Some("https://upstream.com/bucket/file.zip?p1=ok1&p2=ok2".to_string())
);
}

Expand Down
6 changes: 5 additions & 1 deletion src/http/handlers/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ pub async fn fetch(
) -> Result<HttpResponse, Error> {
let get_url = config.create_upstream_url(&req);

if get_url.is_none() {
return not_found();
}

let mut fetch_req = client
.request_from(get_url.as_str(), req.head())
.request_from(get_url.unwrap(), req.head())
.force_close();

let raw_range = req
Expand Down
6 changes: 5 additions & 1 deletion src/http/handlers/forward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ pub async fn forward(
) -> Result<HttpResponse, Error> {
let put_url = config.create_upstream_url(&req);

if put_url.is_none() {
return not_found();
}

let mut forwarded_req = client
.request_from(put_url.as_str(), req.head())
.request_from(put_url.unwrap(), req.head())
.force_close()
.timeout(UPLOAD_TIMEOUT);

Expand Down
8 changes: 8 additions & 0 deletions src/http/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ pub static FETCH_REQUEST_HEADERS_TO_REMOVE: [header::HeaderName; 2] = [
header::CONNECTION,
header::RANGE,
];

pub fn not_found() -> Result<HttpResponse, Error> {
let response = HttpResponse::NotFound()
.insert_header((header::CONTENT_TYPE, "application/json"))
.body("{}");

Ok(response)
}
6 changes: 5 additions & 1 deletion src/http/handlers/simple_proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ pub async fn simple_proxy(
) -> Result<HttpResponse, Error> {
let url = config.create_upstream_url(&req);

let mut proxied_req = client.request_from(url.as_str(), req.head()).force_close();
if url.is_none() {
return not_found();
}

let mut proxied_req = client.request_from(url.unwrap(), req.head()).force_close();

for header in &FETCH_REQUEST_HEADERS_TO_REMOVE {
proxied_req.headers_mut().remove(header);
Expand Down

0 comments on commit 586c60c

Please sign in to comment.