refactor(bare): enhance security and performance optimizations
- Remove unnecessary sorting in advertise_refs for deterministic output - Add path traversal detection and validation in bare_dir construction - Implement symlink resolution checks to prevent security vulnerabilities - Refactor cache system with CRC validation and improved metrics - Integrate repo-specific cache invalidation using indexed keys - Add comprehensive unit tests for commit operations and diff functionality - Move configuration constants to centralized config module - Optimize string operations in disk cache random value generation - Enhance license detection algorithm with cleaner matching logic - Streamline argument processing in various git operations - Update dependencies including crc32fast and flate2 for performance - Add signal handling capability to tokio runtime configuration
This commit is contained in:
+61
-25
@@ -30,6 +30,8 @@ use gix::discover::is_git;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
|
||||
use tonic::codec::CompressionEncoding;
|
||||
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::{GitError, GitResult};
|
||||
use crate::pb::{
|
||||
@@ -151,25 +153,20 @@ impl GitksService {
|
||||
if relative_path.is_empty() {
|
||||
return Err(tonic::Status::invalid_argument("relative_path is required"));
|
||||
}
|
||||
// Validate early to reject '..' and other traversal patterns
|
||||
crate::sanitize::validate_relative_path(relative_path)
|
||||
.map_err(|e| tonic::Status::invalid_argument(e.to_string()))?;
|
||||
|
||||
let candidate = self.repo_prefix.join(relative_path);
|
||||
// Canonicalize repo_prefix (which should exist) for a reliable check
|
||||
let prefix_canon = self
|
||||
.repo_prefix
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| self.repo_prefix.clone());
|
||||
|
||||
// Unified path validation to avoid TOCTOU
|
||||
let canonical = match candidate.canonicalize() {
|
||||
Ok(canon) => {
|
||||
// Path exists and was canonicalized
|
||||
canon
|
||||
}
|
||||
Err(_) => {
|
||||
// Path doesn't exist yet — validate via parent
|
||||
let parent = candidate.parent().unwrap_or(&self.repo_prefix);
|
||||
let filename = candidate.file_name().ok_or_else(|| {
|
||||
tonic::Status::invalid_argument("invalid path: missing filename")
|
||||
@@ -180,7 +177,6 @@ impl GitksService {
|
||||
.unwrap_or_else(|_| parent.to_path_buf());
|
||||
let constructed = parent_canon.join(filename);
|
||||
|
||||
// String-level verification for non-existent paths
|
||||
let constructed_str = constructed.to_string_lossy();
|
||||
let prefix_str = prefix_canon.to_string_lossy();
|
||||
|
||||
@@ -194,13 +190,19 @@ impl GitksService {
|
||||
}
|
||||
};
|
||||
|
||||
// Final check: canonical must be under prefix
|
||||
if !canonical.starts_with(&prefix_canon) {
|
||||
return Err(tonic::Status::invalid_argument(
|
||||
"path traversal detected: relative_path escapes repo prefix",
|
||||
));
|
||||
}
|
||||
|
||||
let double_canon = canonical.canonicalize().unwrap_or_else(|_| canonical.clone());
|
||||
if canonical != double_canon {
|
||||
return Err(tonic::Status::invalid_argument(
|
||||
"path resolved to different target (possible symlink race)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
@@ -211,10 +213,8 @@ impl GitksService {
|
||||
_old_oid: &str,
|
||||
_new_oid: &str,
|
||||
) {
|
||||
// Invalidate moka caches
|
||||
crate::server::cache::invalidate_repo(relative_path);
|
||||
|
||||
// Invalidate disk cache
|
||||
if let Some(ref pc) = self.pack_cache {
|
||||
pc.invalidate_repo(relative_path);
|
||||
}
|
||||
@@ -328,10 +328,8 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> GitResult<std::process::Ou
|
||||
let elapsed = start.elapsed();
|
||||
let elapsed_ms = elapsed.as_millis() as u64;
|
||||
|
||||
// Record metrics
|
||||
crate::metrics::record_git_cmd(cmd_name, elapsed);
|
||||
|
||||
// Slow operation warning
|
||||
if elapsed.as_secs() >= 1 {
|
||||
tracing::warn!(
|
||||
repo = %gb.bare_dir.display(),
|
||||
@@ -343,11 +341,12 @@ pub(crate) fn git_cmd(gb: &GitBare, args: &[&str]) -> GitResult<std::process::Ou
|
||||
|
||||
if !result.status.success() {
|
||||
let stderr_str = String::from_utf8_lossy(&result.stderr);
|
||||
let sanitized = crate::sanitize::sanitize_git_stderr(stderr_str.trim());
|
||||
tracing::warn!(
|
||||
repo = %gb.bare_dir.display(),
|
||||
command = cmd_name,
|
||||
status = ?result.status.code(),
|
||||
stderr = %stderr_str.trim(),
|
||||
stderr = %sanitized,
|
||||
elapsed_ms,
|
||||
"git subprocess exited with non-zero status"
|
||||
);
|
||||
@@ -386,6 +385,19 @@ fn structured_git_error(stderr: &str, code: Option<i32>) -> GitError {
|
||||
pub async fn serve(
|
||||
addr: std::net::SocketAddr,
|
||||
svc: GitksService,
|
||||
) -> Result<(), tonic::transport::Error> {
|
||||
serve_with_shutdown(addr, svc, std::future::pending()).await
|
||||
}
|
||||
|
||||
/// Start the gRPC server and block until the shutdown signal fires.
|
||||
///
|
||||
/// The `shutdown` future should resolve when the process should stop
|
||||
/// (e.g. on SIGTERM/SIGINT). All in-flight requests are drained before
|
||||
/// the server returns.
|
||||
pub async fn serve_with_shutdown(
|
||||
addr: std::net::SocketAddr,
|
||||
svc: GitksService,
|
||||
shutdown: impl std::future::Future<Output = ()>,
|
||||
) -> Result<(), tonic::transport::Error> {
|
||||
let span = tracing::info_span!("gitks.server", %addr);
|
||||
let _enter = span.enter();
|
||||
@@ -393,18 +405,42 @@ pub async fn serve(
|
||||
|
||||
let (health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||
|
||||
let repo_svc = repository_service_server::RepositoryServiceServer::new(svc.clone());
|
||||
let archive_svc = archive_service_server::ArchiveServiceServer::new(svc.clone());
|
||||
let blame_svc = blame_service_server::BlameServiceServer::new(svc.clone());
|
||||
let branch_svc = branch_service_server::BranchServiceServer::new(svc.clone());
|
||||
let commit_svc = commit_service_server::CommitServiceServer::new(svc.clone());
|
||||
let diff_svc = diff_service_server::DiffServiceServer::new(svc.clone());
|
||||
let merge_svc = merge_service_server::MergeServiceServer::new(svc.clone());
|
||||
let pack_svc = pack_service_server::PackServiceServer::new(svc.clone());
|
||||
let ref_svc = ref_service_server::RefServiceServer::new(svc.clone());
|
||||
let remote_svc = remote_service_server::RemoteServiceServer::new(svc.clone());
|
||||
let tag_svc = tag_service_server::TagServiceServer::new(svc.clone());
|
||||
let tree_svc = tree_service_server::TreeServiceServer::new(svc);
|
||||
let repo_svc = repository_service_server::RepositoryServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let archive_svc = archive_service_server::ArchiveServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let blame_svc = blame_service_server::BlameServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let branch_svc = branch_service_server::BranchServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let commit_svc = commit_service_server::CommitServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let diff_svc = diff_service_server::DiffServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let merge_svc = merge_service_server::MergeServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let pack_svc = pack_service_server::PackServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let ref_svc = ref_service_server::RefServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let remote_svc = remote_service_server::RemoteServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let tag_svc = tag_service_server::TagServiceServer::new(svc.clone())
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
let tree_svc = tree_service_server::TreeServiceServer::new(svc)
|
||||
.send_compressed(CompressionEncoding::Gzip)
|
||||
.accept_compressed(CompressionEncoding::Gzip);
|
||||
|
||||
health_reporter
|
||||
.set_serving::<repository_service_server::RepositoryServiceServer<GitksService>>()
|
||||
@@ -458,5 +494,5 @@ pub async fn serve(
|
||||
.add_service(tag_svc)
|
||||
.add_service(tree_svc);
|
||||
tracing::info!("server ready, starting to accept connections");
|
||||
server.serve(addr).await
|
||||
server.serve_with_shutdown(addr, shutdown).await
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user