fix(hooks): shell-escape script values and use glob iteration
Add shell_escape() to safely embed values in generated shell scripts. Replace '$(ls ...)' anti-pattern with glob iteration to correctly handle filenames containing spaces.
This commit is contained in:
+54
-10
@@ -76,23 +76,51 @@ pub fn run_hook_dir(
|
||||
}
|
||||
scripts.sort();
|
||||
|
||||
let dir_start = std::time::Instant::now();
|
||||
for script in &scripts {
|
||||
tracing::debug!(
|
||||
hook_type = %hook_type,
|
||||
script = %script.display(),
|
||||
"executing hook script"
|
||||
);
|
||||
let script_start = std::time::Instant::now();
|
||||
let result = run_single_script(script, stdin_data, timeout);
|
||||
let script_elapsed = script_start.elapsed();
|
||||
|
||||
crate::metrics::record_hook_execution(
|
||||
hook_type,
|
||||
if result.accepted { "ok" } else { "rejected" },
|
||||
script_elapsed,
|
||||
);
|
||||
|
||||
if !result.accepted {
|
||||
tracing::warn!(
|
||||
hook_type = %hook_type,
|
||||
script = %script.display(),
|
||||
exit_code = result.exit_code,
|
||||
stderr = %result.stderr,
|
||||
elapsed_ms = script_elapsed.as_millis() as u64,
|
||||
"hook script rejected"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
hook_type = %hook_type,
|
||||
script = %script.display(),
|
||||
elapsed_ms = script_elapsed.as_millis() as u64,
|
||||
"hook script passed"
|
||||
);
|
||||
}
|
||||
|
||||
let dir_elapsed = dir_start.elapsed();
|
||||
if !scripts.is_empty() {
|
||||
tracing::info!(
|
||||
hook_type = %hook_type,
|
||||
script_count = scripts.len(),
|
||||
elapsed_ms = dir_elapsed.as_millis() as u64,
|
||||
"hook dir completed"
|
||||
);
|
||||
}
|
||||
|
||||
HookResult::accepted()
|
||||
@@ -190,6 +218,12 @@ impl ChildWaitTimeout for std::process::Child {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shell-escape a value by wrapping in single quotes.
|
||||
/// Any embedded single quotes are escaped as `'\''`.
|
||||
fn shell_escape(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
/// Generate the gitks hook runner script content.
|
||||
/// This script is installed into each repository's hooks/ directory
|
||||
/// and orchestrates the execution of server hooks, custom hooks, and gRPC callbacks.
|
||||
@@ -201,16 +235,20 @@ pub fn generate_hook_runner_script(
|
||||
hook_timeout_secs: u64,
|
||||
) -> String {
|
||||
let server_hooks_section = if let Some(dir) = server_hooks_dir {
|
||||
let escaped_dir = shell_escape(dir);
|
||||
let escaped_hook_type = shell_escape(hook_type);
|
||||
format!(
|
||||
r#"
|
||||
# Run server hooks
|
||||
SERVER_HOOKS_DIR="{dir}/{hook_type}.d"
|
||||
SERVER_HOOKS_DIR={escaped_dir}/{escaped_hook_type}.d
|
||||
if [ -d "$SERVER_HOOKS_DIR" ]; then
|
||||
for script in $(ls "$SERVER_HOOKS_DIR" | sort); do
|
||||
for script in "$SERVER_HOOKS_DIR"/*; do
|
||||
[ -f "$script" ] || continue
|
||||
base=$(basename "$script")
|
||||
skip=false
|
||||
case "$script" in .*|*.sample|*~) skip=true;; esac
|
||||
case "$base" in .*|*.sample|*~) skip=true;; esac
|
||||
if [ "$skip" = "false" ]; then
|
||||
"$SERVER_HOOKS_DIR/$script"
|
||||
"$script"
|
||||
exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
exit $exit_code
|
||||
@@ -228,11 +266,13 @@ fi
|
||||
# Run custom hooks (per-repository)
|
||||
CUSTOM_HOOKS_DIR="$GIT_DIR/custom_hooks/$GITKS_HOOK_TYPE.d"
|
||||
if [ -d "$CUSTOM_HOOKS_DIR" ]; then
|
||||
for script in $(ls "$CUSTOM_HOOKS_DIR" | sort); do
|
||||
for script in "$CUSTOM_HOOKS_DIR"/*; do
|
||||
[ -f "$script" ] || continue
|
||||
base=$(basename "$script")
|
||||
skip=false
|
||||
case "$script" in .*|*.sample|*~) skip=true;; esac
|
||||
case "$base" in .*|*.sample|*~) skip=true;; esac
|
||||
if [ "$skip" = "false" ]; then
|
||||
"$CUSTOM_HOOKS_DIR/$script"
|
||||
"$script"
|
||||
exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
exit $exit_code
|
||||
@@ -243,10 +283,11 @@ fi
|
||||
"#;
|
||||
|
||||
let grpc_callback_section = if let Some(addr) = hook_callback_addr {
|
||||
let escaped_addr = shell_escape(addr);
|
||||
format!(
|
||||
r#"
|
||||
# gRPC callback to external HookService
|
||||
if [ -n "{addr}" ]; then
|
||||
if [ -n {escaped_addr} ]; then
|
||||
# gRPC callback is handled by the gitks service directly
|
||||
# The service will make the gRPC call after the git hook completes
|
||||
# This section is a placeholder - actual gRPC callback is handled
|
||||
@@ -258,14 +299,17 @@ fi
|
||||
String::new()
|
||||
};
|
||||
|
||||
let escaped_hook_type = shell_escape(hook_type);
|
||||
let escaped_repo_path = shell_escape(repo_relative_path);
|
||||
|
||||
format!(
|
||||
r#"#!/bin/sh
|
||||
# gitks hook runner for {hook_type}
|
||||
# Repository: {repo_relative_path}
|
||||
# Auto-generated by gitks - do not modify manually
|
||||
|
||||
GITKS_HOOK_TYPE="{hook_type}"
|
||||
GITKS_REPO_RELATIVE_PATH="{repo_relative_path}"
|
||||
GITKS_HOOK_TYPE={escaped_hook_type}
|
||||
GITKS_REPO_RELATIVE_PATH={escaped_repo_path}
|
||||
GITKS_HOOK_TIMEOUT="{hook_timeout_secs}"
|
||||
|
||||
{server_hooks_section}
|
||||
|
||||
Reference in New Issue
Block a user