diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..d274eaf --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,4 @@ +## 2025-03-05 - File permissions and array argument injection in backup scripts +**Vulnerability:** A backup script was creating a temporary directory with default permissions and a zip archive containing sensitive files with default `umask`. Furthermore, an array of exclude patterns with strings separated by spaces was being built and executed via command substitution, leading to word-splitting and the risk of command argument injection for patterns with spaces or shell metacharacters. +**Learning:** Shell scripts generating sensitive archives (like zip backups) must enforce strict access controls on the parent directories (e.g., `chmod 700`) and the unencrypted archives themselves (e.g., via `umask 077` in the generation subshell). Additionally, injecting arguments constructed as space-separated strings into commands creates word-splitting issues and argument injection vulnerabilities, requiring `# shellcheck disable=SC2086` overrides. Arguments must always be built using proper Bash arrays and mapfile/readarray. +**Prevention:** Explicitly set `chmod 700` on backup directories and use `umask 077` subshells when creating archives. Construct arguments dynamically with Bash arrays using `mapfile` to prevent word-splitting and command argument injection vulnerabilities without relying on Shellcheck overrides. diff --git a/tools/backup-projects.sh b/tools/backup-projects.sh index 1b7f6d2..5c80273 100755 --- a/tools/backup-projects.sh +++ b/tools/backup-projects.sh @@ -238,7 +238,9 @@ build_exclude_args() { for pattern in "${EXCLUDE_PATTERNS[@]}"; do args+=("-x" "*/${pattern}/*" "-x" "*/${pattern}") done - echo "${args[@]}" + if [[ ${#args[@]} -gt 0 ]]; then + printf '%s\n' "${args[@]}" + fi } # --- Git Sync --- @@ -267,6 +269,7 @@ sync_git_repos() { local repo_dir repo_dir=$(dirname "$git_dir") local repo_name + # shellcheck disable=SC2034 repo_name=$(basename "$repo_dir") local relative_path="${repo_dir#$HOME/}" @@ -303,7 +306,8 @@ sync_git_repos() { git -C "$repo_dir" add -A 2>/dev/null # Commit with auto-generated message - local commit_msg="chore: auto-backup commit $(date '+%Y-%m-%d %H:%M')" + local commit_msg + commit_msg="chore: auto-backup commit $(date '+%Y-%m-%d %H:%M')" if git -C "$repo_dir" commit -m "$commit_msg" 2>/dev/null; then echo -e " ${GREEN}✓${NC} Committed changes" else @@ -352,6 +356,7 @@ cmd_backup() { if [[ "$DRY_RUN" != true ]]; then mkdir -p "$BACKUP_TEMP_DIR" mkdir -p "$LOG_DIR" + chmod 700 "$BACKUP_TEMP_DIR" "$LOG_DIR" else debug "Would create: $BACKUP_TEMP_DIR" debug "Would create: $LOG_DIR" @@ -406,17 +411,18 @@ cmd_backup() { done fi else - local exclude_args - exclude_args=$(build_exclude_args) + local exclude_args=() + if [[ ${#EXCLUDE_PATTERNS[@]} -gt 0 ]]; then + mapfile -t exclude_args < <(build_exclude_args) + fi ( + umask 077 cd "$HOME" || exit 1 if [[ "$VERBOSE" == true ]]; then - # shellcheck disable=SC2086 - zip -r "$archive_path" "${relative_paths[@]}" $exclude_args + zip -r "$archive_path" "${relative_paths[@]}" "${exclude_args[@]}" else - # shellcheck disable=SC2086 - zip -r -q "$archive_path" "${relative_paths[@]}" $exclude_args + zip -r -q "$archive_path" "${relative_paths[@]}" "${exclude_args[@]}" fi )