d243dce027
- Replaced manual remote client functions with remote_client! macro for archive, blame, branch, commit, and diff services - Simplified remote client creation logic using declarative macro approach - Maintained same functionality while reducing code duplication across services security(bare): enhance path traversal protection with comprehensive validation - Added early relative_path validation to prevent path traversal attacks - Implemented unified path validation to avoid TOCTOU race conditions - Enhanced canonicalization checks for both existing and non-existent paths - Added detailed logging for path traversal detection attempts feat(cache): migrate from CLruCache to Moka with TTL and invalidation support - Replaced clru dependency with moka for improved caching capabilities - Added 300-second time-to-live for cache entries - Implemented repository-specific cache invalidation mechanism - Enhanced cache operations with thread-safe async support refactor(commit): improve security validation for commit operations - Added ref name validation to prevent command injection in cherry_pick_commit - Implemented revision validation for commit selectors - Added comprehensive input validation for create_commit parameters - Enhanced file path validation to prevent traversal
168 lines
6.5 KiB
Rust
168 lines
6.5 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
use crate::error::{GitError, GitResult};
|
|
use crate::pb::RepositoryHeader;
|
|
|
|
#[derive(Debug)]
|
|
pub struct GitBare {
|
|
pub bare_dir: PathBuf,
|
|
}
|
|
|
|
impl GitBare {
|
|
pub fn new(bare_dir: PathBuf) -> Self {
|
|
Self { bare_dir }
|
|
}
|
|
|
|
pub fn gix_repo(&self) -> GitResult<gix::Repository> {
|
|
tracing::debug!(repo = %self.bare_dir.display(), "opening gix repository");
|
|
gix::open(&self.bare_dir).map_err(|e| {
|
|
tracing::error!(repo = %self.bare_dir.display(), error = %e, "failed to open gix repository");
|
|
GitError::Internal(format!("failed to open gix repository: {e}"))
|
|
})
|
|
}
|
|
|
|
pub fn from_repository_header(header: &RepositoryHeader) -> GitResult<Self> {
|
|
let storage_path = header.storage_path.trim();
|
|
let relative_path = header.relative_path.trim();
|
|
let storage_name = header.storage_name.trim();
|
|
let _ = storage_name; // reserved for future sharding logic
|
|
|
|
// Validate relative_path early to prevent path traversal
|
|
if !relative_path.is_empty() {
|
|
crate::sanitize::validate_relative_path(relative_path)?;
|
|
}
|
|
|
|
// Build base path: storage_path if given, else relative_path alone
|
|
let base = if !storage_path.is_empty() {
|
|
let p = Path::new(storage_path);
|
|
if !p.is_absolute() {
|
|
return Err(GitError::InvalidArgument(
|
|
"storage_path must be an absolute path".into(),
|
|
));
|
|
}
|
|
PathBuf::from(p)
|
|
} else if !relative_path.is_empty() {
|
|
return Err(GitError::InvalidArgument(
|
|
"relative_path requires storage_path to be set".into(),
|
|
));
|
|
} else {
|
|
return Err(GitError::InvalidArgument("empty repository path".into()));
|
|
};
|
|
|
|
let bare_dir = if !relative_path.is_empty() && !storage_path.is_empty() {
|
|
let candidate = base.join(relative_path);
|
|
// Canonicalize base (parent dir likely exists) for a reliable traversal check.
|
|
let base_canon = base.canonicalize().unwrap_or_else(|_| base.clone());
|
|
|
|
// Unified path validation to avoid TOCTOU race condition
|
|
let canonical = match candidate.canonicalize() {
|
|
Ok(canon) => {
|
|
// Path exists and was canonicalized successfully
|
|
canon
|
|
}
|
|
Err(_) => {
|
|
// Path doesn't exist yet — validate via parent directory
|
|
// This avoids TOCTOU by not having separate code paths
|
|
let parent = candidate.parent().unwrap_or(&base);
|
|
let filename = candidate.file_name().ok_or_else(|| {
|
|
GitError::InvalidArgument("invalid path: missing filename".into())
|
|
})?;
|
|
|
|
// Canonicalize parent (which should exist)
|
|
let parent_canon = parent
|
|
.canonicalize()
|
|
.unwrap_or_else(|_| parent.to_path_buf());
|
|
|
|
// Construct the full path and verify it's under base
|
|
let constructed = parent_canon.join(filename);
|
|
|
|
// String-level check as fallback for non-existent paths
|
|
let constructed_str = constructed.to_string_lossy();
|
|
let base_str = base_canon.to_string_lossy();
|
|
|
|
if !constructed_str.starts_with(&*base_str) {
|
|
tracing::warn!(
|
|
relative_path = %relative_path,
|
|
base = %base_canon.display(),
|
|
"path traversal attempt detected (parent check)"
|
|
);
|
|
return Err(GitError::InvalidArgument(format!(
|
|
"path traversal detected: {relative_path} escapes storage root"
|
|
)));
|
|
}
|
|
|
|
constructed
|
|
}
|
|
};
|
|
|
|
// Final verification: canonical path must be under base
|
|
if !canonical.starts_with(&base_canon) {
|
|
tracing::warn!(
|
|
relative_path = %relative_path,
|
|
canonical = %canonical.display(),
|
|
base = %base_canon.display(),
|
|
"path traversal attempt detected"
|
|
);
|
|
return Err(GitError::InvalidArgument(format!(
|
|
"path traversal detected: {relative_path} escapes storage root"
|
|
)));
|
|
}
|
|
canonical
|
|
} else if !storage_path.is_empty() {
|
|
base.canonicalize().unwrap_or(base)
|
|
} else {
|
|
return Err(GitError::InvalidArgument("empty repository path".into()));
|
|
};
|
|
|
|
if !bare_dir.exists() {
|
|
tracing::warn!(path = %bare_dir.display(), "repository not found");
|
|
return Err(GitError::RepoNotFound);
|
|
}
|
|
if !bare_dir.is_dir() {
|
|
return Err(GitError::InvalidArgument(format!(
|
|
"not a directory: {}",
|
|
bare_dir.display()
|
|
)));
|
|
}
|
|
|
|
let head_path = bare_dir.join("HEAD");
|
|
if !head_path.exists() {
|
|
let git_dir = bare_dir.join(".git");
|
|
if git_dir.is_dir() && git_dir.join("HEAD").exists() {
|
|
tracing::debug!(path = %git_dir.display(), "resolved non-bare repo via .git subdir");
|
|
return Ok(Self { bare_dir: git_dir });
|
|
}
|
|
return Err(GitError::NotBareRepository);
|
|
}
|
|
|
|
Ok(Self { bare_dir })
|
|
}
|
|
|
|
pub fn object_format(&self) -> crate::pb::ObjectFormat {
|
|
let repo = self.gix_repo().ok();
|
|
let kind = repo
|
|
.as_ref()
|
|
.map(|r| r.object_hash())
|
|
.unwrap_or(gix::hash::Kind::Sha1);
|
|
match kind {
|
|
gix::hash::Kind::Sha1 => crate::pb::ObjectFormat::Sha1,
|
|
gix::hash::Kind::Sha256 => crate::pb::ObjectFormat::Sha256,
|
|
_ => crate::pb::ObjectFormat::Unspecified,
|
|
}
|
|
}
|
|
|
|
/// Convert a hex object id to a protobuf Oid.
|
|
///
|
|
/// `Oid.value` is the binary hash bytes, while `Oid.hex` keeps the printable
|
|
/// lowercase representation for clients that prefer string IDs.
|
|
pub fn oid_to_pb(&self, hex: impl Into<String>) -> crate::pb::Oid {
|
|
let hex = hex.into().to_lowercase();
|
|
let format = self.object_format();
|
|
crate::pb::Oid {
|
|
value: crate::oid::hex_to_bytes(&hex).unwrap_or_default(),
|
|
hex,
|
|
format: format as i32,
|
|
}
|
|
}
|
|
}
|