Skip to content

Commit d1dcdbf

Browse files
authored
Merge pull request #25 from nuclearcat/fixes
Fixes
2 parents 8e3d6a5 + 5d87322 commit d1dcdbf

3 files changed

Lines changed: 88 additions & 25 deletions

File tree

Dockerfile

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
FROM rust:1.83 AS builder
1+
FROM rust:1.93 AS builder
22
WORKDIR /usr/src/app
33
COPY . .
44
RUN cargo install --path .
55

6-
FROM debian:bookworm-slim
6+
FROM debian:trixie-slim
77
RUN apt-get update && rm -rf /var/lib/apt/lists/*
88
COPY --from=builder /usr/local/cargo/bin/kernelci-storage /usr/local/bin/kernelci-storage
99
# install ssl certificates
1010
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
1111
RUN mkdir /workdir
1212
WORKDIR /workdir
1313
CMD ["kernelci-storage"]
14-
15-

src/azure.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ async fn get_file_from_blob(filename: String) -> ReceivedFile {
415415
headers_file_exists = true;
416416
break;
417417
}
418-
std::thread::sleep(std::time::Duration::from_millis(1000));
418+
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
419419
debug_log!(
420420
"Waiting for headers file {} to exist: {} seconds",
421421
cache_filename_headers,

src/main.rs

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ async fn get_or_create_semaphore(locks: &FileSemaphores, filename: &str) -> Arc<
102102
.clone()
103103
}
104104

105+
/// Remove semaphore entries that are no longer in use (strong_count == 1 means
106+
/// only the map itself holds a reference).
107+
async fn cleanup_semaphore(locks: &FileSemaphores, filename: &str) {
108+
let mut map = locks.write().await;
109+
if let Some(sem) = map.get(filename) {
110+
if Arc::strong_count(sem) == 1 {
111+
map.remove(filename);
112+
}
113+
}
114+
}
115+
105116
struct ReceivedFile {
106117
original_filename: String,
107118
cached_filename: String,
@@ -491,6 +502,13 @@ prefixes = ["/bob"]
491502
prefixes = [""]
492503
*/
493504

505+
fn validate_path(path: &str) -> Result<(), String> {
506+
if path.contains("..") {
507+
return Err("Path traversal detected".to_string());
508+
}
509+
Ok(())
510+
}
511+
494512
fn verify_upload_permissions(owner: &str, path: &str) -> Result<(), String> {
495513
let cfg_content = get_config_content();
496514
let cfg: Table = toml::from_str(&cfg_content).unwrap();
@@ -559,9 +577,19 @@ async fn ax_post_file(
559577
let mut file0_filename: String = "".to_string();
560578
let mut upload_result: Option<(StatusCode, Vec<u8>)> = None;
561579
let mut buffered_file: Option<tempfile::NamedTempFile> = None;
580+
let mut locked_path: Option<String> = None;
562581

563-
while let Some(field) = multipart.next_field().await.unwrap() {
564-
let name = field.name().unwrap().to_string();
582+
while let Some(field) = match multipart.next_field().await {
583+
Ok(field) => field,
584+
Err(e) => {
585+
eprintln!("Error reading multipart field: {:?}", e);
586+
return (StatusCode::BAD_REQUEST, b"Malformed multipart request".to_vec());
587+
}
588+
} {
589+
let name = match field.name() {
590+
Some(name) => name.to_string(),
591+
None => continue,
592+
};
565593
let filename = field.file_name().map(|f| f.to_string());
566594

567595
if name == "path" {
@@ -658,6 +686,15 @@ async fn ax_post_file(
658686

659687
let full_path = format!("{}/{}", path, file0_filename);
660688

689+
// validate path for traversal
690+
match validate_path(&full_path) {
691+
Ok(_) => (),
692+
Err(e) => {
693+
upload_result = Some((StatusCode::BAD_REQUEST, e.into_bytes()));
694+
break;
695+
}
696+
}
697+
661698
// verify upload permissions
662699
match verify_upload_permissions(&owner, &path) {
663700
Ok(_) => (),
@@ -669,6 +706,7 @@ async fn ax_post_file(
669706

670707
let hdr_content_type = headers.get("Content-Type-Upstream");
671708
let semaphore = get_or_create_semaphore(&state.file_locks, &full_path).await;
709+
locked_path = Some(full_path.clone());
672710

673711
// Try to acquire permit - wait for up to 30 seconds
674712
let _permit = match tokio::time::timeout(
@@ -764,6 +802,11 @@ async fn ax_post_file(
764802
}
765803
}
766804

805+
// Clean up semaphore from the fast path (permit already dropped by leaving the loop)
806+
if let Some(ref lp) = locked_path {
807+
cleanup_semaphore(&state.file_locks, lp).await;
808+
}
809+
767810
// Handle buffered file0 case (file0 arrived before path)
768811
if upload_result.is_none() {
769812
if let Some(tmp) = buffered_file {
@@ -781,6 +824,13 @@ async fn ax_post_file(
781824

782825
let full_path = format!("{}/{}", path, file0_filename);
783826

827+
match validate_path(&full_path) {
828+
Ok(_) => (),
829+
Err(e) => {
830+
return (StatusCode::BAD_REQUEST, e.into_bytes());
831+
}
832+
}
833+
784834
match verify_upload_permissions(&owner, &path) {
785835
Ok(_) => (),
786836
Err(e) => {
@@ -839,6 +889,9 @@ async fn ax_post_file(
839889

840890
// tmp (NamedTempFile) is dropped here, auto-deleting the temp file
841891

892+
drop(_permit);
893+
cleanup_semaphore(&state.file_locks, &full_path).await;
894+
842895
if result.is_empty() {
843896
return (StatusCode::CONFLICT, Vec::new());
844897
}
@@ -926,6 +979,11 @@ async fn ax_get_file(
926979

927980
// IMPORTANT! Headers in cache must be stored in lowercase
928981
let received_file = driver_get_file(filepath.clone());
982+
983+
// Release the semaphore now that the file is resolved
984+
drop(_permit);
985+
cleanup_semaphore(&state.file_locks, &filepath).await;
986+
929987
if !received_file.valid {
930988
println!(
931989
"{:?} 404 0 {} {} {} {}",
@@ -997,7 +1055,9 @@ async fn ax_get_file(
9971055

9981056
/* Usually HEAD is used to check if the file exists and range is supported */
9991057
if method == axum::http::Method::HEAD {
1000-
//println!("HEAD request, returning headers only");
1058+
if let Ok(val) = header::HeaderValue::from_str(&metadata.len().to_string()) {
1059+
headers.insert(header::CONTENT_LENGTH, val);
1060+
}
10011061
println!(
10021062
"{:?} 200 0 {} {} {} {}",
10031063
remote_addr, human_time, method, filepath, user_agent_str
@@ -1011,7 +1071,12 @@ async fn ax_get_file(
10111071
let mut end = metadata.len();
10121072
// is Content-Range present?
10131073
if let Some(range) = rxheaders.get("Range") {
1014-
(start, end) = parse_range(range.to_str().unwrap());
1074+
match range.to_str().ok().and_then(parse_range) {
1075+
Some(parsed) => (start, end) = parsed,
1076+
None => {
1077+
return (StatusCode::RANGE_NOT_SATISFIABLE, "Malformed Range header").into_response();
1078+
}
1079+
}
10151080
}
10161081
// if start is set to non-zero, we need to seek
10171082
if start != 0 && (end == metadata.len() || end == 0) {
@@ -1096,29 +1161,29 @@ fn write_file_driver(
10961161

10971162
/// Parse range header
10981163
/// We support limited range only for now
1099-
fn parse_range(range: &str) -> (u64, u64) {
1100-
let parts: Vec<&str> = range.split("=").collect();
1101-
let range_parts: Vec<&str> = parts[1].split("-").collect();
1102-
let start = range_parts[0].parse::<u64>().unwrap();
1103-
if range_parts.len() == 1 {
1104-
return (start, 0);
1105-
}
1106-
let end = range_parts[1].parse::<u64>();
1107-
match end {
1108-
Ok(end) => (start, end),
1109-
Err(_) => (start, 0),
1110-
}
1164+
fn parse_range(range: &str) -> Option<(u64, u64)> {
1165+
let (_, range_spec) = range.split_once('=')?;
1166+
let (start_str, end_str) = range_spec.split_once('-')?;
1167+
let start = start_str.parse::<u64>().ok()?;
1168+
let end = end_str.parse::<u64>().unwrap_or(0);
1169+
Some((start, end))
11111170
}
11121171

11131172
/// Verify the Authorization header
11141173
/// Return error message + owner if the token is correct
11151174
fn verify_auth_hdr(headers: &HeaderMap) -> Result<String, Option<String>> {
1116-
let auth = headers.get("Authorization");
1117-
if auth == None {
1175+
let auth = match headers.get("Authorization") {
1176+
Some(auth) => auth,
1177+
None => return Err(None),
1178+
};
1179+
let auth_str = match auth.to_str() {
1180+
Ok(s) => s,
1181+
Err(_) => return Err(None),
1182+
};
1183+
let token_parts: Vec<&str> = auth_str.split_whitespace().collect();
1184+
if token_parts.is_empty() {
11181185
return Err(None);
11191186
}
1120-
let token = auth.unwrap().to_str().unwrap().split_whitespace();
1121-
let token_parts: Vec<&str> = token.collect();
11221187
if token_parts.len() != 2 {
11231188
let verif_result = storjwt::verify_jwt_token(token_parts[0]);
11241189
let bmap = match verif_result {

0 commit comments

Comments
 (0)