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

Add some validation for remove_job_data in the executor server #468

Merged
merged 2 commits into from
Nov 1, 2022
Merged
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
93 changes: 86 additions & 7 deletions ballista/executor/src/executor_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use ballista_core::BALLISTA_VERSION;
use std::collections::HashMap;
use std::convert::TryInto;
use std::ops::Deref;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
Expand Down Expand Up @@ -720,13 +720,92 @@ impl<T: 'static + AsLogicalPlan, U: 'static + AsExecutionPlan> ExecutorGrpc
request: Request<RemoveJobDataParams>,
) -> Result<Response<RemoveJobDataResult>, Status> {
let job_id = request.into_inner().job_id;
let work_dir = self.executor.work_dir.clone();
let mut path = PathBuf::from(work_dir);
path.push(job_id.clone());
if path.is_dir() {
info!("Remove data for job {:?}", job_id);
std::fs::remove_dir_all(&path)?;
info!("Remove data for job {:?}", job_id);

let work_dir = PathBuf::from(&self.executor.work_dir);
let mut path = work_dir.clone();
path.push(&job_id);

// Verify it's an existing directory
if !path.is_dir() {
return if !path.exists() {
Err(Status::invalid_argument(format!(
"Path {:?} does not exist!!!",
path
)))
} else {
Err(Status::invalid_argument(format!(
"Path {:?} is not for a directory!!!",
path
)))
};
}

if !is_subdirectory(path.as_path(), work_dir.as_path()) {
return Err(Status::invalid_argument(format!(
"Path {:?} is not a subdirectory of {:?}!!!",
path, work_dir
)));
}

std::fs::remove_dir_all(&path)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs we have a vulnerability here:

if self has a verbatim prefix (e.g. \\?\C:\windows) and path is not empty, the new path is normalized: all references to . and .. are removed.

I can bypass the . string check on Windows by doing a \\?. It is impossible to validate directories at a string level. The missing check is to fully normalize this directory, then check if the normalized directory is a subdirectory of the work_dir.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. I'll refine it to use a more standard and robust way for the subdirectory check.


Ok(Response::new(RemoveJobDataResult {}))
}
}

// Check whether the path is the subdirectory of the base directory
fn is_subdirectory(path: &Path, base_path: &Path) -> bool {
if let (Ok(path), Ok(base_path)) = (path.canonicalize(), base_path.canonicalize()) {
if let Some(parent_path) = path.parent() {
parent_path.starts_with(base_path)
} else {
false
}
} else {
false
}
}

#[cfg(test)]
mod test {
use crate::executor_server::is_subdirectory;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::TempDir;

#[tokio::test]
async fn test_is_subdirectory() {
let base_dir = TempDir::new().unwrap().into_path();

// Normal correct one
{
let job_path = prepare_testing_job_directory(&base_dir, "job_a");
assert!(is_subdirectory(&job_path, base_dir.as_path()));
}

// Empty job id
{
let job_path = prepare_testing_job_directory(&base_dir, "");
assert!(!is_subdirectory(&job_path, base_dir.as_path()));

let job_path = prepare_testing_job_directory(&base_dir, ".");
assert!(!is_subdirectory(&job_path, base_dir.as_path()));
}

// Malicious job id
{
let job_path = prepare_testing_job_directory(&base_dir, "..");
assert!(!is_subdirectory(&job_path, base_dir.as_path()));
}
}

fn prepare_testing_job_directory(base_dir: &Path, job_id: &str) -> PathBuf {
let mut path = base_dir.to_path_buf();
path.push(job_id);
if !path.exists() {
fs::create_dir(&path).unwrap();
}
path
}
}